diff --git a/.bazelrc b/.bazelrc index 3bf3651332..908109170e 100644 --- a/.bazelrc +++ b/.bazelrc @@ -42,6 +42,7 @@ build:remote --@rules_rust//rust/settings:extra_rustc_flag=-Clink-arg=-fuse-ld=b build:remote --@rules_rust//rust/settings:extra_exec_rustc_flag=-Clink-arg=-fuse-ld=bfd build:remote --action_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin build:remote --test_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +build:remote --strategy=TestRunner=local build:remote --remote_download_minimal build:remote --jobs=50 build:remote --remote_cache_compression @@ -63,6 +64,7 @@ build:trusted-seed --@rules_rust//rust/settings:extra_rustc_flag=-Clink-arg=-fus build:trusted-seed --@rules_rust//rust/settings:extra_exec_rustc_flag=-Clink-arg=-fuse-ld=bfd build:trusted-seed --action_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin build:trusted-seed --test_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +build:trusted-seed --strategy=TestRunner=local build:trusted-seed --remote_upload_local_results=true build:trusted-seed --remote_cache_async=false build:trusted-seed --remote_download_minimal diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd74a595d3..025dc27287 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,7 +3,7 @@ /Cargo.toml @vicondoa /Cargo.lock @vicondoa /packages/Cargo.guest.lock @vicondoa -/packages/d2b-priv-broker/Cargo.toml @vicondoa +/packages/d2b-broker/Cargo.toml @vicondoa /packages/d2b-guest-shell-runner/Cargo.toml @vicondoa /packages/policy-inputs/** @vicondoa /packages/policy-inputs/advisory-policy.json @vicondoa @@ -13,7 +13,7 @@ /packages/xtask/tests/policy_production_closure.rs @vicondoa /Makefile @vicondoa /flake.nix @vicondoa -/nixos-modules/guest-control.nix @vicondoa +/nixos-modules/component-session.nix @vicondoa /nixos-modules/host-activation.nix @vicondoa /nixos-modules/host-broker.nix @vicondoa /nixos-modules/host-daemon.nix @vicondoa diff --git a/.github/workflows/pr-l1-static-fast.yml b/.github/workflows/pr-l1-static-fast.yml index cc624858de..9ae9584ee4 100644 --- a/.github/workflows/pr-l1-static-fast.yml +++ b/.github/workflows/pr-l1-static-fast.yml @@ -107,24 +107,6 @@ jobs: - name: Guest shell runner suite run: make test-rust-guest-shell-runner - rust-local: - runs-on: ubuntu-latest - timeout-minutes: 90 - env: - D2B_BAZEL_PROFILE: local - D2B_BAZEL_UNTRUSTED: "1" - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - with: - persist-credentials: false - - uses: cachix/install-nix-action@23cf0fec1d55e0b1f2631aedd2a610c21ef8b077 - with: - nix_path: nixpkgs=channel:nixos-unstable - extra_nix_config: | - experimental-features = nix-command flakes - - name: Local Rust suite - run: make test-rust-local - nix-eval: runs-on: ubuntu-latest timeout-minutes: 45 @@ -163,7 +145,7 @@ jobs: nix-realized: runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 120 env: D2B_BAZEL_PROFILE: local D2B_BAZEL_UNTRUSTED: "1" @@ -218,7 +200,7 @@ jobs: test-performance-budgets: runs-on: ubuntu-latest continue-on-error: true - timeout-minutes: 10 + timeout-minutes: 30 env: D2B_BAZEL_PROFILE: local D2B_BAZEL_UNTRUSTED: "1" @@ -236,7 +218,7 @@ jobs: check: if: ${{ always() }} - needs: [tier0, policy-tooling, rust-main, rust-broker, rust-guest, rust-local, nix-eval, nix-unit, nix-realized, nix-aarch64, fixtures-proofs] + needs: [tier0, policy-tooling, rust-main, rust-broker, rust-guest, nix-eval, nix-unit, nix-realized, nix-aarch64, fixtures-proofs] runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 diff --git a/.github/workflows/release-host-binaries.yml b/.github/workflows/release-host-binaries.yml index 61183a9b69..3aeaea9b1d 100644 --- a/.github/workflows/release-host-binaries.yml +++ b/.github/workflows/release-host-binaries.yml @@ -105,7 +105,7 @@ jobs: packages/d2b-wayland-proxy/Cargo.toml \ packages/d2b-unsafe-local-helper/Cargo.toml \ packages/d2b-host/Cargo.toml \ - packages/d2b-priv-broker/Cargo.toml + packages/d2b-broker/Cargo.toml do package_version=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) [ "$package_version" = "$VERSION" ] || { @@ -126,7 +126,7 @@ jobs: } manifest_version=$(jq -r 'if .version == null then "null" else (.version | strings) end' nix/prebuilt.json) - expected_keys='["d2b","d2b-activation-helper","d2b-priv-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' + expected_keys='["d2b","d2b-activation-helper","d2b-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' if [ "$manifest_version" = "null" ]; then jq -e '.version == null and .system == "x86_64-linux" and .binaries == {}' \ nix/prebuilt.json >/dev/null || { @@ -145,7 +145,7 @@ jobs: for binary in \ d2b \ d2b-activation-helper \ - d2b-priv-broker \ + d2b-broker \ d2b-unsafe-local-helper \ d2b-wayland-proxy \ d2bd @@ -246,7 +246,7 @@ jobs: packages/d2b-wayland-proxy/Cargo.toml \ packages/d2b-unsafe-local-helper/Cargo.toml \ packages/d2b-host/Cargo.toml \ - packages/d2b-priv-broker/Cargo.toml + packages/d2b-broker/Cargo.toml do package_version=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) [ "$package_version" = "$VERSION" ] || { @@ -341,7 +341,7 @@ jobs: --package d2b-host --bin d2b-activation-helper rustup run "$PINNED" cargo build --release --locked \ --manifest-path Cargo.toml \ - --package d2b-priv-broker --bin d2b-priv-broker --no-default-features + --package d2b-broker --bin d2b-broker --no-default-features rm -rf dist mkdir -p dist/bin @@ -350,12 +350,12 @@ jobs: cp target/release/d2b-wayland-proxy dist/bin/ cp target/release/d2b-unsafe-local-helper dist/bin/ cp target/release/d2b-activation-helper dist/bin/ - cp target/release/d2b-priv-broker dist/bin/ + cp target/release/d2b-broker dist/bin/ for binary in \ d2b \ d2b-activation-helper \ - d2b-priv-broker \ + d2b-broker \ d2b-unsafe-local-helper \ d2b-wayland-proxy \ d2bd @@ -461,7 +461,7 @@ jobs: packages/d2b-wayland-proxy/Cargo.toml \ packages/d2b-unsafe-local-helper/Cargo.toml \ packages/d2b-host/Cargo.toml \ - packages/d2b-priv-broker/Cargo.toml + packages/d2b-broker/Cargo.toml do package_version=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) [ "$package_version" = "$VERSION" ] || { @@ -473,7 +473,7 @@ jobs: flake_versions=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' flake.nix | sort -u) [ "$(printf '%s\n' "$flake_versions" | awk -v expected="$VERSION" '$0 != expected && NF { count++ } END { print count + 0 }')" -eq 0 ] - expected_keys='["d2b","d2b-activation-helper","d2b-priv-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' + expected_keys='["d2b","d2b-activation-helper","d2b-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' manifest_version=$(jq -r 'if .version == null then "null" else (.version | strings) end' nix/prebuilt.json) if [ "$manifest_version" = "null" ]; then jq -e '.version == null and .system == "x86_64-linux" and .binaries == {}' nix/prebuilt.json >/dev/null @@ -492,7 +492,7 @@ jobs: for binary in \ d2b \ d2b-activation-helper \ - d2b-priv-broker \ + d2b-broker \ d2b-unsafe-local-helper \ d2b-wayland-proxy \ d2bd @@ -612,7 +612,7 @@ jobs: asset_names=( "d2b-${VERSION}-x86_64-linux.tar.gz" "d2b-activation-helper-${VERSION}-x86_64-linux.tar.gz" - "d2b-priv-broker-${VERSION}-x86_64-linux.tar.gz" + "d2b-broker-${VERSION}-x86_64-linux.tar.gz" "d2b-unsafe-local-helper-${VERSION}-x86_64-linux.tar.gz" "d2b-wayland-proxy-${VERSION}-x86_64-linux.tar.gz" "d2bd-${VERSION}-x86_64-linux.tar.gz" @@ -703,7 +703,7 @@ jobs: case "$1" in "d2b-${VERSION}-x86_64-linux.tar.gz" \ | "d2b-activation-helper-${VERSION}-x86_64-linux.tar.gz" \ - | "d2b-priv-broker-${VERSION}-x86_64-linux.tar.gz" \ + | "d2b-broker-${VERSION}-x86_64-linux.tar.gz" \ | "d2b-unsafe-local-helper-${VERSION}-x86_64-linux.tar.gz" \ | "d2b-wayland-proxy-${VERSION}-x86_64-linux.tar.gz" \ | "d2bd-${VERSION}-x86_64-linux.tar.gz" \ diff --git a/AGENTS.md b/AGENTS.md index 923acf4cf4..aeab952b4a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ configuration, start with [`README.md`](README.md). d2b is an opinionated NixOS desktop microVM framework for a single trusted Wayland host and untrusted, isolated workloads. Its daemon-only control plane -is `d2bd` plus `d2b-priv-broker`; its declarative modules own networking, +is `d2bd` plus `d2b-broker`; its declarative modules own networking, per-VM store views, mediated devices, and versioned bundle contracts. Product direction is in [`STRATEGY.md`](./STRATEGY.md); the threat model is in [`docs/explanation/design.md`](./docs/explanation/design.md). Binding @@ -205,7 +205,7 @@ Read the relevant section before changing any of these: ## Daemon-only end-state (P6 onward) d2b declares exactly three root-visible units: `d2bd.service`, -`d2b-priv-broker.socket`, and `d2b-priv-broker.service`. The binding decision +`d2b-broker.socket`, and `d2b-broker.service`. The binding decision is [ADR 0015](./docs/adr/0015-daemon-only-clean-break.md). - `d2bd` supervises every per-VM DAG. The broker dispatches audited host diff --git a/BUILD.bazel b/BUILD.bazel index 65c17a9d79..8b60a282d0 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -102,7 +102,6 @@ exports_files([ "docs/explanation/design.md", "docs/how-to/migrate-d2b-v1-0-to-v1-1.md", "docs/reference/cli-contract.md", - "nixos-modules/guest-control-token-materialize.py", "docs/reference/cli-output/list.schema.json", "docs/reference/cli-output/status.schema.json", "docs/reference/components-usbip.md", @@ -145,8 +144,7 @@ exports_files([ "nixos-modules/generated/options-zones-Zone.nix", "nixos-modules/generated/options-zones-ZoneLink.nix", "nixos-modules/gateway-vm.nix", - "nixos-modules/guest-control.nix", - "nixos-modules/guest-control-host.nix", + "nixos-modules/component-session.nix", "nixos-modules/host-activation.d/state-dir-acl.sh", "nixos-modules/host-activation.nix", "nixos-modules/host-daemon.nix", @@ -296,21 +294,19 @@ filegroup( "//packages/d2b-core:cargo_workspace_sources", "//packages/d2b-core-controller:cargo_workspace_sources", "//packages/d2b-core/fuzz:BUILD.bazel", - "//packages/d2b-cutover:cargo_workspace_sources", - "//packages/d2b-exec-runner:cargo_workspace_sources", "//packages/d2b-gateway:cargo_workspace_sources", "//packages/d2b-gateway-runtime:cargo_workspace_sources", "//packages/d2b-guest-shell-runner:cargo_workspace_sources", - "//packages/d2b-guestd:cargo_workspace_sources", "//packages/d2b-host:cargo_workspace_sources", "//packages/d2b-host-activation-helper:cargo_workspace_sources", - "//packages/d2b-priv-broker:cargo_workspace_sources", + "//packages/d2b-broker:cargo_workspace_sources", "//packages/d2b-process:cargo_workspace_sources", "//packages/d2b-process-conformance:cargo_workspace_sources", "//packages/d2b-provider:cargo_workspace_sources", "//packages/d2b-provider-activation-nixos:cargo_workspace_sources", "//packages/d2b-provider-audio-pipewire:cargo_workspace_sources", "//packages/d2b-provider-clipboard-wayland:cargo_workspace_sources", + "//packages/d2b-provider-config-nixos:cargo_workspace_sources", "//packages/d2b-provider-credential-entra:cargo_workspace_sources", "//packages/d2b-provider-credential-managed-identity:cargo_workspace_sources", "//packages/d2b-provider-credential-secret-service:cargo_workspace_sources", diff --git a/CHANGELOG.md b/CHANGELOG.md index cf251ad120..4d13a36f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -290,8 +290,7 @@ deprecations ship one minor release before removal. with CLI projections, Guest-resident Entrablau identity custody, fast hermetic tests with integration-only slow coverage, and Noise-protected ComponentSession/d2b-bus channels on the `d2bus.org` public namespace, - together with the security/threat-model, reset/cutover, feasibility, - reset, cutover, and feasibility contracts. No crates, services, + together with the security/threat-model and feasibility contracts. No crates, services, controllers, or Providers are created. @@ -481,8 +480,7 @@ deprecations ship one minor release before removal. - Added macvtap-backed external network attachment for env net VMs, including independent egress NAT, port forwards, and mDNS/`.local` reflection. - Added generated storage and synchronization contracts, read-only startup - validation, `d2b host doctor --read-only`, and - `d2b host migrate-storage --dry-run`. + validation, and `d2b host doctor --read-only`. - Added provider-aware graceful VM shutdown with configurable global and per-VM timeouts, plus explicit `--force` lifecycle overrides. - Added experimental remote full-host and provider-managed Azure Container Apps @@ -796,7 +794,7 @@ deprecations ship one minor release before removal. no-orphan; a `setsid`/double-fork escapee is a documented trusted-root limitation). Interactive detached exec remains unsupported; use non-TTY `d2b vm exec -d` for detached commands. See - [`docs/reference/guest-control-exec-interactive-tty.md`](docs/reference/guest-control-exec-interactive-tty.md) + [`docs/reference/cli-contract.md`](docs/reference/cli-contract.md#exec) and the interactive-exec section of [ADR 0028](docs/adr/0028-guest-control-plane-over-vsock.md). The guest-control wire contract is unchanged (the TTY surface was already diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 105392d1f9..0692b68c8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,7 +116,7 @@ When docs disagree with committed, passing code, the code wins. Update the docs ## Host-prepare gates Contributors touching anything in `packages/d2b-host/`, -`packages/d2b-priv-broker/src/ops/`, or the host-prepare +`packages/d2b-broker/src/ops/`, or the host-prepare docs (`docs/how-to/host-prepare.md`, `docs/how-to/host-prepare.d/*.md`, `docs/reference/{cgroup-delegation,inet-d2b-chains,privileges,support-matrix}.md`) diff --git a/Cargo.lock b/Cargo.lock index 49766f0a32..234ea125c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,7 +986,7 @@ dependencies = [ "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core", - "d2b-cutover", + "d2b-provider-config-nixos", "d2b-realm-core", "d2b-resource-client", "d2b-zone-routing", @@ -1010,6 +1010,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-resource", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + [[package]] name = "d2b-bus" version = "0.0.0-bootstrap" @@ -1017,9 +1041,11 @@ dependencies = [ "async-trait", "d2b-audit", "d2b-contracts", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-controller-toolkit", + "d2b-core-controller", "d2b-resource-api", "d2b-resource-store", "d2b-resource-store-redb", @@ -1027,6 +1053,7 @@ dependencies = [ "d2b-session-unix", "d2b-telemetry", "getrandom 0.3.4", + "protobuf", "quote", "serde_json", "sha2", @@ -1034,6 +1061,7 @@ dependencies = [ "syn 2.0.119", "tempfile", "tokio", + "ttrpc", ] [[package]] @@ -1055,7 +1083,6 @@ name = "d2b-contracts-broker" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", - "d2b-contracts-control", "d2b-contracts-resource", "schemars", "serde", @@ -1069,7 +1096,6 @@ version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", "d2b-contracts-resource", - "protobuf", "schemars", "serde", "serde_json", @@ -1175,25 +1201,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "d2b-cutover" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-resource", - "nix 0.29.0", - "serde", - "serde_json", -] - -[[package]] -name = "d2b-exec-runner" -version = "0.0.0-bootstrap" -dependencies = [ - "rustix 0.38.44", -] - [[package]] name = "d2b-gateway" version = "0.0.0-bootstrap" @@ -1249,25 +1256,6 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "d2b-guestd" -version = "0.0.0-bootstrap" -dependencies = [ - "async-trait", - "d2b-contracts", - "d2b-contracts-control", - "d2b-exec-runner", - "d2b-realm-core", - "futures", - "hmac", - "protobuf", - "rustix 0.38.44", - "sha2", - "tokio", - "tokio-vsock", - "ttrpc", -] - [[package]] name = "d2b-host" version = "0.0.0-bootstrap" @@ -1296,32 +1284,6 @@ dependencies = [ "libc", ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - [[package]] name = "d2b-process" version = "0.0.0-bootstrap" @@ -1338,6 +1300,7 @@ dependencies = [ "d2b-contracts-zone-session", "serde", "serde_json", + "sha2", ] [[package]] @@ -1360,6 +1323,7 @@ dependencies = [ "d2b-contracts-zone-session", "serde", "serde_json", + "sha2", ] [[package]] @@ -1367,6 +1331,7 @@ name = "d2b-provider-audio-pipewire" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "libc", @@ -1403,6 +1368,21 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts-resource", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "ttrpc", +] + [[package]] name = "d2b-provider-credential-entra" version = "0.0.0-bootstrap" @@ -1471,6 +1451,7 @@ version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", "d2b-contracts-broker", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core", @@ -1487,12 +1468,11 @@ dependencies = [ "d2b-contracts", "d2b-contracts-control", "d2b-contracts-resource", - "d2b-contracts-zone-session", "d2b-core", "d2b-provider-toolkit", "d2b-realm-core", + "d2b-session", "env_logger", - "getrandom 0.2.17", "libc", "log", "nix 0.29.0", @@ -1663,11 +1643,13 @@ dependencies = [ name = "d2b-provider-system-systemd" version = "0.0.0-bootstrap" dependencies = [ + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-process-conformance", "d2b-provider-toolkit", "serde", + "serde_json", "tokio", ] @@ -1719,16 +1701,13 @@ name = "d2b-provider-transport-vsock" version = "0.0.0-bootstrap" dependencies = [ "async-trait", - "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", - "d2b-provider", "d2b-session", "ring", "serde", "serde_json", "tokio", - "tracing", ] [[package]] @@ -1791,8 +1770,11 @@ name = "d2b-resource-client" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", + "d2b-core-controller", + "serde_json", "tokio", ] @@ -1938,7 +1920,6 @@ dependencies = [ name = "d2bd" version = "0.0.0-bootstrap" dependencies = [ - "arc-swap", "async-trait", "clap", "d2b-audit", @@ -1951,7 +1932,6 @@ dependencies = [ "d2b-contracts-zone-session", "d2b-core", "d2b-core-controller", - "d2b-cutover", "d2b-gateway", "d2b-gateway-runtime", "d2b-host", @@ -1961,14 +1941,17 @@ dependencies = [ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-device-security-key", "d2b-provider-device-tpm", "d2b-provider-device-usbip", "d2b-provider-display-wayland", "d2b-provider-network-local", "d2b-provider-notification-desktop", + "d2b-provider-observability-otel", "d2b-provider-runtime-azure-container-apps", "d2b-provider-runtime-cloud-hypervisor", + "d2b-provider-shell-terminal", "d2b-provider-supervisor", "d2b-provider-system-core", "d2b-provider-system-minijail", @@ -1988,20 +1971,16 @@ dependencies = [ "notify-rust", "parking_lot", "protobuf", - "regex", "rustix 0.38.44", - "semver", "serde", "serde_json", "sha2", "socket2 0.5.10", - "subtle", "tempfile", "tokio", "tracing", "tracing-subscriber", "ttrpc", - "uzers", ] [[package]] @@ -2015,6 +1994,7 @@ dependencies = [ "d2b-contracts", "d2b-contracts-broker", "d2b-contracts-control", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core", @@ -2038,11 +2018,9 @@ dependencies = [ "serde_json", "sha2", "socket2 0.5.10", - "subtle", "tempfile", "tokio", "tracing", - "tracing-subscriber", "ttrpc", "uzers", ] diff --git a/Cargo.toml b/Cargo.toml index 5b0cfded2c..9425f3b38f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,7 @@ members = [ "packages/d2b-gateway", "packages/d2b-gateway-runtime", "packages/d2b-host", - "packages/d2b-guestd", "packages/d2b-unsafe-local-helper", - "packages/d2b-exec-runner", "packages/d2b-sk-frontend", "packages/d2b-audit", "packages/d2b-telemetry", @@ -26,6 +24,7 @@ members = [ "packages/d2b-provider", "packages/d2b-provider-toolkit", "packages/d2b-provider-activation-nixos", + "packages/d2b-provider-config-nixos", "packages/d2b-provider-audio-pipewire", "packages/d2b-provider-clipboard-wayland", "packages/d2b-provider-display-wayland", @@ -65,9 +64,8 @@ members = [ "packages/d2b", "packages/d2bd-runtime", "packages/d2bd", - "packages/d2b-cutover", "packages/xtask", - "packages/d2b-priv-broker", + "packages/d2b-broker", "packages/d2b-guest-shell-runner", ] @@ -134,7 +132,7 @@ schemars = { version = "0.8", features = ["derive"] } # so scope agents have everything they need to pull these crates in. rustix = { version = "0.38", features = ["fs", "process", "net", "pipe", "system", "mount", "thread", "pty", "termios", "stdio"] } # W3 s2: declared in workspace.dependencies so future scope work (and -# the eventual production netlink backend in d2b-priv-broker) can +# the eventual production netlink backend in d2b-broker) can # opt into rtnetlink via `{ workspace = true }`. Not yet consumed by # any workspace member, so the resolved Cargo.lock is unchanged. The # trait-based netlink backend in `d2b_host::netlink` lets the L1c diff --git a/MODULE.bazel b/MODULE.bazel index 9084d425d0..ebed1fb922 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -115,15 +115,6 @@ crate.annotation( use_repo(crate, "crates") proof_crates = use_extension("@rules_rs//rs:extensions.bzl", "crate") -proof_crates.from_cargo( - name = "proof_chunked", - cargo_lock = "//proofs/chunked-stdio-conformance:Cargo.lock", - cargo_toml = "//proofs/chunked-stdio-conformance:Cargo.toml", - platform_triples = [ - "aarch64-unknown-linux-gnu", - "x86_64-unknown-linux-gnu", - ], -) proof_crates.from_cargo( name = "proof_redb", cargo_lock = "//proofs/redb-resource-store-spike:Cargo.lock", @@ -153,7 +144,6 @@ proof_crates.from_cargo( ) use_repo( proof_crates, - "proof_chunked", "proof_redb", "proof_w0_ch_connect", "proof_window_identity", diff --git a/Makefile b/Makefile index 9eab8c41a1..65e8faae4f 100644 --- a/Makefile +++ b/Makefile @@ -11,21 +11,18 @@ D2B_MAKE_BAZEL_TARGETS := \ check check-fast check-tier0 bazel-check test-unit \ test-lint test-rust test-rust-main test-rust-broker \ - test-rust-guest-shell-runner test-rust-local test-rust-no-bash-ast \ + test-rust-guest-shell-runner \ test-rust-schema test-rust-supply-chain test-rust-leaf-main-workspace \ test-rust-leaf-schema test-rust-leaf-fixture-contracts test-rust-leaf-broker \ - test-rust-leaf-guest-shell-runner test-rust-leaf-no-bash-ast \ + test-rust-leaf-guest-shell-runner \ test-rust-leaf-supply-chain test-fixture-contracts test-proofs test-flake \ test-flake-realized test-flake-aarch64 test-flake-x86 test-nix-unit \ test-performance-budgets test-drift test-policy test-changelog D2B_MAKE_LOCAL_TARGETS := \ check-ci test-integration test-host-integration perf \ - pre-tag smoke-lite heavy-check heavy-test-integration \ - heavy-test-host-integration heavy-flake-check heavy-lane-integration \ - heavy-lane-host-integration heavy-lane-perf heavy-lane-pre-tag \ - heavy-lane-smoke-lite heavy-lane-guard + pre-tag smoke-lite heavy-check heavy-flake-check # Meta helpers that invoke Bazel directly but are not Layer-1 test aliases. -D2B_MAKE_UTILITY_TARGETS := heavy-gate-build changelog-fold +D2B_MAKE_UTILITY_TARGETS := changelog-fold D2B_MAKE_GOALS := $(if $(strip $(MAKECMDGOALS)),$(MAKECMDGOALS),$(.DEFAULT_GOAL)) D2B_MAKE_CLASSIFIED_GOALS := $(filter \ @@ -79,23 +76,19 @@ SHELL := $(CURDIR)/tests/tools/scrub-shell-environment bazel-check \ test-unit \ test-lint test-rust test-rust-main \ - test-rust-broker test-rust-guest-shell-runner test-rust-local test-rust-no-bash-ast \ + test-rust-broker test-rust-guest-shell-runner \ test-rust-schema test-rust-supply-chain \ test-rust-leaf-main-workspace \ test-rust-leaf-schema \ test-rust-leaf-fixture-contracts test-rust-leaf-broker \ - test-rust-leaf-guest-shell-runner test-rust-leaf-no-bash-ast \ + test-rust-leaf-guest-shell-runner \ test-rust-leaf-supply-chain \ test-fixture-contracts test-proofs test-flake test-flake-realized \ test-flake-aarch64 test-flake-x86 test-nix-unit \ test-performance-budgets \ test-drift test-policy test-changelog \ test-integration test-host-integration perf \ - heavy-lane-guard heavy-lane-integration heavy-lane-host-integration \ - heavy-lane-perf \ - heavy-lane-pre-tag heavy-lane-smoke-lite \ - heavy-gate-build heavy-gate-provision heavy-check heavy-flake-check \ - heavy-test-integration heavy-test-host-integration \ + heavy-check heavy-flake-check \ clean # Current Nix system double, used to address per-system flake.checks attrs. @@ -112,8 +105,8 @@ SYSTEM ?= $(shell nix eval --extra-experimental-features 'nix-command flakes' \ # make test- focused Bazel suite. # make test-integration type-9 container integration; local host/manual pre-PR. # make test-host-integration type-10 runNixOSTest; local NixOS/KVM pre-PR. -# make heavy- the same lane, serialized through the two-slot -# per-uid heavy-gate semaphore (see "Heavy lanes"). +# make heavy-check full Layer-1 check. +# make heavy-flake-check full flake realization. # =========================================================================== ## check-ci - run the Layer-1 gate, then the conditional container lane. @@ -146,16 +139,8 @@ $(D2B_MAKE_BAZEL_TARGETS): # Sub-targets. Each target is a thin alias over one public Bazel suite. # =========================================================================== -## test-integration - L2 podman container integration tests. Public heavy lane: -## it acquires a heavy-gate slot, then runs the raw work behind the gate so it -## can never oversubscribe a concurrent lane, even when invoked directly or via -## `make check-ci`. -test-integration: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-integration - -## heavy-lane-integration - the raw L2 container work. Internal: reachable only -## from inside the gate (see heavy-lane-guard). -heavy-lane-integration: heavy-lane-guard +## test-integration - L2 podman container integration tests. +test-integration: bash tests/test-integration.sh # =========================================================================== @@ -170,12 +155,8 @@ heavy-lane-integration: heavy-lane-guard ## successor to the `D2B_LIVE`-against-the-real-host scripts. Needs KVM (a local ## NixOS host; TCG software emulation is the slow fallback when /dev/kvm is ## absent). x86_64-linux only (a same-system VM builder is required). -## Public heavy lane: acquires a slot, then runs the raw work behind the gate. ## Set D2B_VM_CHECK= to build one named vmChecks entry. -test-host-integration: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-host-integration - -heavy-lane-host-integration: heavy-lane-guard +test-host-integration: @set -eu; \ system="$$(nix eval --raw --impure --expr builtins.currentSystem)"; \ if [ "$$system" != "x86_64-linux" ]; then \ @@ -251,10 +232,9 @@ heavy-lane-host-integration: heavy-lane-guard '$(BAZEL_BIN)' build --config=local \ //packages/d2b:d2b \ //packages/d2bd:d2bd \ - //packages/d2b-priv-broker:d2b-priv-broker \ + //packages/d2b-broker:d2b-broker \ //packages/d2b-host:d2b-activation-helper \ //packages/d2b-host-activation-helper:d2b-host-activation-helper \ - //packages/d2b-cutover:d2b-cutover-runner \ //packages/d2b-unsafe-local-helper:d2b-unsafe-local-helper \ //packages/d2b-resource-compiler:d2b-resource-compiler \ //packages/d2b-provider-display-wayland:d2b-wayland-proxy; \ @@ -264,10 +244,9 @@ heavy-lane-host-integration: heavy-lane-guard stage_tool() { source="$$(realpath -e "$$bazel_bin/$$1")"; case "$$source" in "$$bazel_bin"/*) ;; *) echo "test-host-integration: Bazel output escaped bazel-bin" >&2; return 1;; esac; [ -f "$$source" ] && [ -x "$$source" ] || { echo "test-host-integration: invalid Bazel output $$1" >&2; return 1; }; install -m 755 "$$source" "$$stage/$$2"; }; \ stage_tool packages/d2b/d2b d2b; \ stage_tool packages/d2bd/d2bd d2bd; \ - stage_tool packages/d2b-priv-broker/d2b-priv-broker d2b-priv-broker; \ + stage_tool packages/d2b-broker/d2b-broker d2b-broker; \ stage_tool packages/d2b-host/d2b-activation-helper d2b-activation-helper; \ stage_tool packages/d2b-host-activation-helper/d2b-host-activation-helper d2b-host-activation-helper; \ - stage_tool packages/d2b-cutover/d2b-cutover-runner d2b-cutover-runner; \ stage_tool packages/d2b-unsafe-local-helper/d2b-unsafe-local-helper d2b-unsafe-local-helper; \ stage_tool packages/d2b-resource-compiler/d2b-resource-compiler d2b-resource-compiler; \ stage_tool packages/d2b-provider-display-wayland/d2b-wayland-proxy d2b-wayland-proxy; \ @@ -287,7 +266,12 @@ heavy-lane-host-integration: heavy-lane-guard echo "test-host-integration: could not resolve a vmCheck derivation for Attic" >&2; \ exit 1; \ }; \ - if ! nix-store -qR --include-outputs "$$drv" >>"$$run_dir/attic-closure-all"; then \ + if [ "$$drv" = "unknown-deriver" ]; then \ + closure_source="$$output"; \ + else \ + closure_source="$$drv"; \ + fi; \ + if ! nix-store -qR --include-outputs "$$closure_source" >>"$$run_dir/attic-closure-all"; then \ echo "test-host-integration: could not resolve a vmCheck dependency closure for Attic" >&2; \ exit 1; \ fi; \ @@ -306,136 +290,30 @@ heavy-lane-host-integration: heavy-lane-guard echo "test-host-integration: Attic closure upload succeeded"; \ fi -## Public heavy lanes: acquire a slot, then run the raw work behind the gate. -perf: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-perf - -heavy-lane-perf: heavy-lane-guard +## perf - run the advisory performance budget suite. +perf: $(BAZEL_RUN) //bazel/checks:test-performance-budgets -## heavy-lane-guard - fail closed when a heavy-lane internal target is invoked -## outside the gate. It does not trust the mere presence of D2B_HEAVY_GATE -## (any process can export that); instead it asks the wrapper to verify that -## this process genuinely holds a slot via its open file description lock. -## -## verify-slot reports its verdict purely through its exit status, so branch on -## the typed codes rather than collapsing every nonzero status into one: -## -## 0 a genuinely held slot -> proceed -## 3 no slot is held -> reacquire by running the PUBLIC lane -## (which acquires a slot and re-runs -## this lane through the gate). A shared -## prerequisite cannot exec that itself -## without double-running the parent -## recipe, so guide the operator to the -## acquiring entrypoint and fail closed -## with the typed unheld code. -## * the verifier itself malfunctioned -> propagate the exact code unchanged -## and fail closed -## -## Collapsing 3 and every malfunction into one "outside the semaphore" exit hid -## a broken gate behind a slot-bypass message; keeping the codes distinct lets a -## caller tell "ran the raw target directly" apart from "the verifier is broken". -heavy-lane-guard: heavy-gate-build - @rc=0; $(HEAVY_GATE_BIN) heavy-gate verify-slot || rc=$$?; \ - if [ "$$rc" -eq 0 ]; then \ - exit 0; \ - elif [ "$$rc" -eq 3 ]; then \ - echo "heavy lane invoked outside the heavy-gate semaphore (no slot held)." >&2; \ - echo "Run the public lane (e.g. 'make test-integration'), which acquires a slot" >&2; \ - echo "and re-runs this lane through the gate; do not run the internal target directly." >&2; \ - exit "$$rc"; \ - else \ - echo "heavy-gate verify-slot failed closed (exit $$rc); refusing to run heavy work unsynchronised." >&2; \ - exit "$$rc"; \ - fi - -# =========================================================================== -# Heavy lanes. -# -# Every Layer-2, host-integration, live, and perf-heavy command runs -# through ONE semaphore: `xtask heavy-gate`. It grants two slots per uid -# via open file description locks, so concurrent lanes cannot oversubscribe the -# shared Nix store or KVM device. Do not add a second -# lock file, sleep-and-retry loop, or per-crate heavy-lane guard. -# -# Run the heavy-* target instead of the bare target whenever another heavy lane -# might be running; the bare targets stay available for a serial console. -# =========================================================================== - BAZEL_BIN ?= $(if $(D2B_BAZEL_BIN),$(D2B_BAZEL_BIN),bazel) -HEAVY_GATE_BIN := $(CURDIR)/bazel-bin/packages/xtask/xtask -HEAVY_GATE = $(HEAVY_GATE_BIN) heavy-gate -- - -## heavy-gate-build - build the semaphore wrapper through its Bazel owner. -heavy-gate-build: - '$(BAZEL_BIN)' build --config=local //packages/xtask:xtask - -## heavy-gate-provision - create or repair the protected slot namespace for the -## current numeric uid without resolving a user name through NSS. This is the -## explicit post-login path for network-backed users and the developer setup -## path on hosts that do not consume the NixOS module. Because /run is a tmpfs, -## run it once per boot when the gate reports missing provisioning. It never -## creates a fallback under a user-owned root. -heavy-gate-provision: - @target_uid="$$(id -u)"; \ - sudo -- sh -eu -c '\ - target_uid="$$1"; root=/run/d2b-heavy-gates; \ - case "$$target_uid" in ""|*[!0-9]*) echo "heavy-gate provisioning: invalid target uid" >&2; exit 1;; esac; \ - if [ -L "$$root" ] || { [ -e "$$root" ] && [ ! -d "$$root" ]; }; then echo "heavy-gate provisioning: refusing an unsafe runtime root" >&2; exit 1; fi; \ - install -d -m 0755 -o root -g root "$$root"; \ - uid_dir="$$root/uid-$$target_uid"; \ - if [ -L "$$uid_dir" ] || { [ -e "$$uid_dir" ] && [ ! -d "$$uid_dir" ]; }; then echo "heavy-gate provisioning: refusing an unsafe per-user directory" >&2; exit 1; fi; \ - install -d -m 0755 -o root -g root "$$uid_dir"; \ - for index in 0 1; do \ - slot="$$uid_dir/slot-$$index"; \ - if [ -L "$$slot" ] || { [ -e "$$slot" ] && [ ! -f "$$slot" ]; }; then echo "heavy-gate provisioning: refusing an unsafe slot file" >&2; exit 1; fi; \ - if [ ! -e "$$slot" ]; then install -m 0600 -o "$$target_uid" -g root /dev/null "$$slot"; else chown "$$target_uid":root "$$slot"; chmod 0600 "$$slot"; fi; \ - done; \ - echo "heavy-gate provisioning: protected slots are ready for this boot"' \ - sh "$$target_uid" - -## heavy-check - the Layer-1 PR-equivalent gate under the heavy-lane semaphore. -heavy-check: heavy-gate-build - $(HEAVY_GATE) $(BAZEL_RUN) //bazel/checks:check - -## heavy-test-integration / -host-integration - explicit aliases for -## the public heavy lanes, kept for muscle memory and scripts. The public lanes -## now acquire the semaphore themselves; a redundant outer gate here is safe -## because the inner invocation verifies and reuses the inherited slot. -heavy-test-integration: test-integration -heavy-test-host-integration: test-host-integration +## heavy-check - the complete Layer-1 check. +heavy-check: + $(BAZEL_RUN) //bazel/checks:check -## heavy-flake-check - the building `nix flake check` under the semaphore. -## `make test-flake` is the cheap --no-build sibling. -heavy-flake-check: heavy-gate-build - $(HEAVY_GATE) $(NIX_FLAKE) flake check --print-build-logs +## heavy-flake-check - the building `nix flake check`; `make test-flake` is the +## cheap --no-build sibling. +heavy-flake-check: + $(NIX_FLAKE) flake check --print-build-logs # --- pre-existing maintainer targets --------------------------------------- ## pre-tag - run the full live-VM smoke gate before tagging a release. ## Requires: KVM, d2b active, both personal-dev and work-aad VMs declared. ## Exits non-zero on any probe failure. Updates $${TMPDIR:-/tmp}/d2b-smoke-run-log.txt. -## Public heavy lane: acquires a slot, then runs the raw live work behind -## the gate - the live smoke suite is the most destructive, stateful lane -## in the tree and must never bypass the sole-use semaphore. -pre-tag: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-pre-tag - -## heavy-lane-pre-tag - the raw full live-VM smoke work. Internal: reachable only -## from inside the gate (see heavy-lane-guard). -heavy-lane-pre-tag: heavy-lane-guard +pre-tag: bash tests/integration/live/live-vm-smoke.sh --full ## smoke-lite - run the single-VM lite smoke gate (≤5 min). -## Public heavy lane: acquires a slot, then runs the raw live work -## behind the gate. -smoke-lite: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-smoke-lite - -## heavy-lane-smoke-lite - the raw lite live-VM smoke work. Internal: reachable -## only from inside the gate (see heavy-lane-guard). -heavy-lane-smoke-lite: heavy-lane-guard +smoke-lite: bash tests/integration/live/live-vm-smoke.sh --lite .PHONY: changelog-fold diff --git a/README.md b/README.md index bec75cb252..35562eb5f0 100644 --- a/README.md +++ b/README.md @@ -326,8 +326,9 @@ Guest configuration is still built on the host, but guest activation stays inside the guest. The host build produces the VM's NixOS `system.build.toplevel`; d2b's broker/store-view path publishes that closure into the per-VM live store pool; virtiofs serves that pool -as the guest's `/nix/store`; and guestd activates the prepared toplevel -inside the running VM for `switch`, `test`, and live `rollback`. +as the guest's `/nix/store`; and the target-local activation Process applies +the prepared toplevel inside the running VM for `switch`, `test`, and live +`rollback`. Stopped VMs do not run live activation from the host: use `d2b boot --apply` to stage the declared toplevel for the next start. @@ -397,27 +398,22 @@ The Rust `d2b` CLI is the only operator surface. Run - **Not yet implemented**: `console`, `audio status|mic|speaker|off` return a typed exit-78 envelope until the daemon-native surface ships. Argument parsing and shell completions still work. -- **Guest control** (admin-only): `vm exec` runs a command inside a - VM over the authenticated guest-control vsock - no SSH - +- **Guest Process execution** (admin-only): `vm exec` runs a command inside a + VM through the authenticated ComponentSession - no SSH - (`d2b exec run Guest/ -- `). It is restricted to callers in `d2b.site.adminUsers`, the role gate enforced via `SO_PEERCRED` at the daemon socket. -To enable guest exec on a VM: set `d2b.vms..guest.control.enable -= true` and `guest.exec.enable = true` (the VM must have a workload user -via `ssh.user`); add your operator account to `d2b.site.adminUsers`; -rebuild and let the notify-ready daemon restart into the new generation -(or run `sudo systemctl restart d2bd` explicitly); -then start the VM on the guest-control generation and run the verbs. -Every exec runs the requested command as the VM's workload user -(`ssh.user`) - **never root** - inside a real PAM login session, so -graphical and login-shell workflows see the same environment an SSH -login would (`XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, the login-shell -profile). Operators elevate with `sudo` inside the session. +To enable the Guest target agent on a VM, set +`d2b.vms..guest.componentSession.enable = true`; add your operator +account to `d2b.site.adminUsers`, then rebuild and restart the VM. Exec +requests are admitted as target-local `Process` resources and executed only +from signed Provider templates; no caller-supplied SSH transport or host +process is involved. Run-state ships in `/var/lib/d2b/`; per-host config emitted by the NixOS module ships in `/etc/d2b/` (bundle + privileges + -processes JSON files consumed by `d2bd` / `d2b-priv-broker`). +processes JSON files consumed by `d2bd` / `d2b-broker`). For typed exit codes and JSON envelopes, see [`docs/reference/cli-contract.md`](docs/reference/cli-contract.md). diff --git a/SECURITY.md b/SECURITY.md index 51d60aabe0..89ab4e0d58 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -76,7 +76,7 @@ See [the unsafe-local provider contract](docs/reference/unsafe-local-provider.md ### Portability roadmap The portability work introduces a non-root `d2bd` daemon plus a -minimal root-owned `d2b-priv-broker` (see ADRs 0001-0008 under +minimal root-owned `d2b-broker` (see ADRs 0001-0008 under [`docs/adr/`](docs/adr/)). The new trust boundaries the daemon work will introduce are: @@ -105,8 +105,8 @@ will introduce are: > capabilities (`capabilities = []`), `requiresStartRoot = false`, > and a `userNamespace` block mapping in-NS UID/GID 0 to the > per-share principal. Normal VM shares map to the per-VM runner - > principal; the guest-control token share maps to the narrower - > `d2b--gctlfs` principal. The broker pre-establishes the + > principal; ComponentSession enrollment keys are not exposed through + > a virtiofs share. The broker pre-establishes the > namespace via `clone3(CLONE_NEWUSER)` + `/proc//uid_map` > writes before exec; virtiofsd runs fake-root only inside the > per-share user NS. This is strictly stronger than v1.1.1: a @@ -164,7 +164,7 @@ The new trust-boundary statements are: NetworkManager unmanaged config, and `modprobe` state on behalf of `d2bd`, gated entirely by the closed broker enum plus the trusted bundle. Every operation has a typed handler under - `packages/d2b-priv-broker/src/ops/` and re-derives its + `packages/d2b-broker/src/ops/` and re-derives its operating paths from the bundle, never from caller input. - Compromise of `d2bd` cannot escalate to arbitrary host mutation beyond the declared broker enum variants. Unknown @@ -195,25 +195,21 @@ The new trust-boundary statements are: trust-boundary delta; only the per-busid `UsbipBindFirewallRule` skeleton is covered. -### Guest-control exec trust boundary +### ComponentSession exec trust boundary `d2b vm exec` runs a command inside a VM over -the authenticated guest-control vsock channel - there is no SSH. The +the authenticated ComponentSession channel - there is no SSH. The trust-boundary statements are: - **Admin-only, destructive.** Guest exec is a destructive verb: the `SO_PEERCRED` caller must be in `d2b.site.adminUsers` (the daemon-side role gate above), on top of the `d2b`-group - connection gate. Per-VM exec must also be enabled in the bundle - (`guest.control.enable` + `guest.exec.enable`). Every exec runs the - requested command as the VM's workload user (`ssh.user`) - **never - root** - inside a real PAM login session (`systemd-run - --property=PAMName=login --uid=`); the wire `user` field is - host-fixed by guestd and ignored, and operators elevate with `sudo` - inside the session. + connection gate. Every exec is admitted as a target-local `Process` or + `EphemeralProcess` from a signed Provider template; caller-supplied SSH + transport and host process execution are not accepted. - **Leak-safe daemon-side audit.** The daemon records attached exec - lifecycle events (`GuestControlExecEstablished` / - `GuestControlExecTerminated`) to its own + lifecycle events (`component_session_exec_established` / + `component_session_exec_terminated`) to its own `daemon-events-.jsonl`, carrying ONLY the VM name, the admin `peer_uid`, and the negotiated `tty` shape. Detached create and kill/cancel write separate redacted daemon audit events carrying ONLY diff --git a/STRATEGY.md b/STRATEGY.md index 7d28ef5544..129619e071 100644 --- a/STRATEGY.md +++ b/STRATEGY.md @@ -18,7 +18,7 @@ applications remain integrated with the host desktop. ## Ownership and architecture d2b owns its microVM substrate end to end. The daemon-only control plane is -`d2bd` plus `d2b-priv-broker`: the daemon supervises lifecycle DAGs and the +`d2bd` plus `d2b-broker`: the daemon supervises lifecycle DAGs and the privileged broker performs audited host mutations. The framework declares no per-VM systemd templates and keeps runner processes behind typed broker operations. diff --git a/TODO.md b/TODO.md index b952c6f85a..c7b79b8a02 100644 --- a/TODO.md +++ b/TODO.md @@ -211,7 +211,7 @@ config with no reservation for the new VM. **Symptom.** The new VM boots (cloud-hypervisor + all sidecars reach process-alive), its tap attaches to `br--lan`, but it never gets its reserved DHCP lease, so it never reaches the network. d2b's -`guest-ssh-readiness` node times out at the deadline and the whole VM +`component-session-health` node times out at the deadline and the whole VM start **rolls back**. From the operator's seat the new VM "just won't start" with no obvious cause - the real fault is a stale sibling `sys--net`. Reproduced 2026-06-07 adding `work-ssd` to the `work` @@ -335,7 +335,7 @@ alongside the per-op privileges matrix enforcement. ## Per-op privileges-matrix enforcement at broker (Phase 6 security-hardening) -`packages/d2b-priv-broker/src/runtime.rs` documents the Phase A +`packages/d2b-broker/src/runtime.rs` documents the Phase A runner-control trust model above the `SignalRunner` and `DeregisterRunnerPidfd` handlers: ADR 0015 treats `d2bd` as part of the daemon-only TCB, `envelope.caller_role` is audit-only at the @@ -418,7 +418,7 @@ All in comments / doc-strings, e.g. `packages/d2b-host/src/virtiofsd_argv.rs`, `packages/d2b-host/src/swtpm_argv.rs`, `packages/d2b-host/src/gpu_argv.rs`, -`packages/d2b-priv-broker/src/ops/spawn_runner.rs`, +`packages/d2b-broker/src/ops/spawn_runner.rs`, `packages/d2bd/src/pidfs_probe.rs`, `packages/d2bd/src/ch_stats.rs`, `packages/d2b-core/src/bundle_resolver.rs` (e.g. line 2265 @@ -577,7 +577,7 @@ inconsistency). The supervisor decision logic needs to either: **Files to start from.** -- `packages/d2b-priv-broker/src/sys.rs` - the reaper claims to +- `packages/d2b-broker/src/sys.rs` - the reaper claims to use `waitid(P_PIDFD)` + signalfd; verify it runs. - `packages/d2bd/src/supervisor/pidfd.rs` - `PidfdTable::snapshot` writes the file; check where entries are @@ -664,7 +664,7 @@ operator's login was a hard prerequisite for invoking the CLI. **Files / code paths to start from.** -- `packages/d2b-priv-broker/src/live_handlers.rs` - `SpawnRunner` +- `packages/d2b-broker/src/live_handlers.rs` - `SpawnRunner` handler; the audio policy ref is `w1-audio`. - `packages/d2bd/src/supervisor/pidfd.rs` - pidfd lifecycle; this is where the respawn-on-death watchdog needs to land. diff --git a/bazel/checks/BUILD.bazel b/bazel/checks/BUILD.bazel index 78e273afc6..a4abe8b660 100644 --- a/bazel/checks/BUILD.bazel +++ b/bazel/checks/BUILD.bazel @@ -21,11 +21,8 @@ test_suite( "//packages/d2b-controller-toolkit:all-tests", "//packages/d2b-core:all-tests", "//packages/d2b-core-controller:all-tests", - "//packages/d2b-cutover:all-tests", - "//packages/d2b-exec-runner:all-tests", "//packages/d2b-gateway:all-tests", "//packages/d2b-gateway-runtime:all-tests", - "//packages/d2b-guestd:all-tests", "//packages/d2b-host:all-tests", "//packages/d2b-host-activation-helper:all-tests", "//packages/d2b-process:all-tests", @@ -93,26 +90,15 @@ test_suite( tests = ["//bazel/checks/rust:portable_rust_guest"], ) -test_suite( - name = "test-rust-local", - tests = ["//bazel/checks/rust:portable_rust_local"], -) - test_suite( name = "test-rust", tests = [ ":test-rust-main", ":test-rust-broker", ":test-rust-guest-shell-runner", - ":test-rust-local", ], ) -test_suite( - name = "test-rust-no-bash-ast", - tests = ["//tests/tools/no-bash-ast-walker:no_bash_ast_test"], -) - test_suite( name = "test-rust-schema", tests = ["//packages/xtask:schema_reproducibility_test"], @@ -148,11 +134,6 @@ test_suite( tests = [":test-rust-guest-shell-runner"], ) -test_suite( - name = "test-rust-leaf-no-bash-ast", - tests = [":test-rust-no-bash-ast"], -) - test_suite( name = "test-rust-leaf-supply-chain", tests = [":test-rust-supply-chain"], diff --git a/bazel/checks/fixtures/BUILD.bazel b/bazel/checks/fixtures/BUILD.bazel index 3f10741bbb..bec0e3e3f3 100644 --- a/bazel/checks/fixtures/BUILD.bazel +++ b/bazel/checks/fixtures/BUILD.bazel @@ -6,6 +6,7 @@ _FIXTURE_SOURCES = [ "//:bazel_provider_workspace_sources", "//:Cargo.lock", "//:Cargo.toml", + "//:d2b_resource_schemas_v3", "//:examples_workspace_sources", "//:flake.lock", "//:flake.nix", diff --git a/bazel/checks/nix/BUILD.bazel b/bazel/checks/nix/BUILD.bazel index 93f94af3c6..a690d6a648 100644 --- a/bazel/checks/nix/BUILD.bazel +++ b/bazel/checks/nix/BUILD.bazel @@ -16,14 +16,31 @@ _SURFACES = { "nixos-modules/processes-json.nix", ], }, - "guest-control": { - "cases": [], + "broker-profiles": { + "cases": ["guest-broker", "host-tools-source", "prebuilt-broker"], + "package": "bazel/checks/fixtures", + "files": [ + "bazel/checks/fixtures/BUILD.bazel", + "docs/reference/schemas/v3/providers/transport-azure-relay.transport-settings.json", + "docs/reference/schemas/v3/providers/transport-vsock.transport-binding.json", + "nixos-modules/rust-host-tools.nix", + "nixos-modules/vm-evaluator.nix", + "nix/prebuilt.json", + "nix/prebuilt.nix", + "nixos-modules/guest-broker.nix", + "nixos-modules/host-broker.nix", + "nixos-modules/prebuilt-packages.nix", + ], + "modules": [], + }, + "component-session": { + "cases": ["gateway-component-session", "guest-component-session"], "files": [ "nixos-modules/vm-evaluator.nix", ], "modules": [ - "nixos-modules/guest-control.nix", - "nixos-modules/guest-control-host.nix", + "nixos-modules/component-session.nix", + "nixos-modules/guest-broker.nix", "nixos-modules/processes-json.nix", "nixos-modules/vm-guest-base.nix", ], @@ -87,10 +104,11 @@ _SURFACES = { ], }, "provider-catalog": { - "cases": ["provider-elf-shim"], + "cases": ["provider-catalog", "provider-elf-shim"], "package": "packages/d2b-provider-activation-nixos", "files": [ "nix/provider-elf-shim.nix", + "nixos-modules/generated/provider-catalog-shape.nix", ], "modules": [ "nixos-modules/provider-catalog.nix", @@ -366,6 +384,15 @@ nix_native_test( timeout = "eternal", ) +nix_native_test( + name = "flake-eval-x86-realized-guest-static", + command = 'build --no-write-lock-file --no-link --print-out-paths "$FLAKE_REF#checks.x86_64-linux.guest-static-elf" >/dev/null', + lane = "flake-realized-guest-static", + max_kib = 14665373, + data = ["//:flake.lock"], + timeout = "eternal", +) + nix_native_test( name = "flake-eval-aarch64", command = 'eval --no-write-lock-file --raw "$FLAKE_REF#checks.aarch64-linux.eval-minimal.drvPath" >/dev/null', @@ -386,6 +413,7 @@ test_suite( ":flake-eval-x86-realized-gas-city", ":flake-eval-x86-realized-supply-chain", ":flake-eval-x86-realized-package-output", + ":flake-eval-x86-realized-guest-static", ], ) diff --git a/bazel/checks/policy/BUILD.bazel b/bazel/checks/policy/BUILD.bazel index d814c04d72..6a65a20565 100644 --- a/bazel/checks/policy/BUILD.bazel +++ b/bazel/checks/policy/BUILD.bazel @@ -4,7 +4,6 @@ test_suite( name = "source_hygiene", tests = [ "//bazel/checks/meta:tier0", - "//tests/tools/no-bash-ast-walker:no_bash_ast_test", ], ) @@ -36,9 +35,6 @@ test_suite( test_suite( name = "proofs", tests = [ - "//proofs/chunked-stdio-conformance:clippy", - "//proofs/chunked-stdio-conformance:doc", - "//proofs/chunked-stdio-conformance:unit", "//proofs/redb-resource-store-spike:clippy", "//proofs/redb-resource-store-spike:doc", "//proofs/redb-resource-store-spike:full_scale_smoke", diff --git a/bazel/checks/rust/BUILD.bazel b/bazel/checks/rust/BUILD.bazel index fa99d3d537..6ec46dc7e9 100644 --- a/bazel/checks/rust/BUILD.bazel +++ b/bazel/checks/rust/BUILD.bazel @@ -3,24 +3,27 @@ package(default_visibility = ["//visibility:public"]) test_suite( name = "portable_rust_broker", tests = [ - "//packages/d2b-priv-broker:d2b_priv_broker_doc_test", - "//packages/d2b-priv-broker:bridge_lifecycle", - "//packages/d2b-priv-broker:broker_export_audit", - "//packages/d2b-priv-broker:broker_protocol_compatibility", - "//packages/d2b-priv-broker:broker_socket_acl", - "//packages/d2b-priv-broker:bundle_tampered_broker", - "//packages/d2b-priv-broker:d2b_priv_broker_fake_backends_test", - "//packages/d2b-priv-broker:d2b_priv_broker_layer1_bootstrap_test", - "//packages/d2b-priv-broker:d2b_priv_broker_test", - "//packages/d2b-priv-broker:kernel_surface", - "//packages/d2b-priv-broker:persistent_tap_lifecycle", - "//packages/d2b-priv-broker:pidfd_handoff_scm_rights", - "//packages/d2b-priv-broker:pidfd_real_spawner", - "//packages/d2b-priv-broker:security_key_broker", - "//packages/d2b-priv-broker:socket_activation", - "//packages/d2b-priv-broker:w12_fd_passing_response", - "//packages/d2b-priv-broker:w15_install_migrate", - "//packages/d2b-priv-broker:zone_store", + "//packages/d2b-broker:d2b_broker_doc_test", + "//packages/d2b-broker:bridge_lifecycle", + "//packages/d2b-broker:broker_export_audit", + "//packages/d2b-broker:broker_protocol_compatibility", + "//packages/d2b-broker:broker_socket_acl", + "//packages/d2b-broker:bundle_tampered_broker", + "//packages/d2b-broker:guest_profile", + "//packages/d2b-broker:host_profile", + "//packages/d2b-broker:d2b_broker_fake_backends_test", + "//packages/d2b-broker:d2b_broker_layer1_bootstrap_test", + "//packages/d2b-broker:d2b_broker_test", + "//packages/d2b-broker:kernel_surface", + "//packages/d2b-broker:persistent_tap_lifecycle", + "//packages/d2b-broker:pidfd_handoff_scm_rights", + "//packages/d2b-broker:pidfd_real_spawner", + "//packages/d2b-broker:profile_separation", + "//packages/d2b-broker:security_key_broker", + "//packages/d2b-broker:socket_activation", + "//packages/d2b-broker:w12_fd_passing_response", + "//packages/d2b-broker:w15_install_migrate", + "//packages/d2b-broker:zone_store", ], ) @@ -35,25 +38,6 @@ test_suite( ], ) -test_suite( - name = "portable_rust_local", - tests = [ - "//packages/d2b:cli_json_contract", - "//packages/d2b:stub_no_socket", - "//packages/d2b-host:guest_control_token_materializer", - "//packages/d2b-unsafe-local-helper:d2b_unsafe_local_helper_test", - "//packages/d2b-unsafe-local-helper:shell_supervisor", - "//packages/d2bd:d2bd_lib_test", - "//packages/d2bd:daemon_socket_acl", - "//packages/d2bd:daemon_state_lock", - "//packages/d2bd:daemon_state_persistence", - "//packages/d2bd:stub_no_socket", - "//packages/d2b-bus:production_watch_rss", - "//packages/xtask:buildbuddy_config", - "//packages/xtask:heavy_gate_reexec", - ], -) - filegroup( name = "source_hygiene_sources", srcs = [ diff --git a/changelog.d/2026-08-19-cutover-preview-consistency.md b/changelog.d/2026-08-19-cutover-preview-consistency.md deleted file mode 100644 index c195da630c..0000000000 --- a/changelog.d/2026-08-19-cutover-preview-consistency.md +++ /dev/null @@ -1,10 +0,0 @@ -### Fixed - -- Keep host cutover preview digests recovery-free while retaining recovery - binding in apply requests and consent. -- Secure the one-shot cutover bootstrap descriptor across broker exec without - allowing it to leak into later runner children. -- Grant lifecycle traversal only on the runtime socket directories while - keeping cutover state root-only. -- Add configured Admin users to the `d2b` lifecycle group without widening - realm-only access. diff --git a/changelog.d/2026-08-19-d2b-cutover.md b/changelog.d/2026-08-19-d2b-cutover.md deleted file mode 100644 index 9fdfd6d434..0000000000 --- a/changelog.d/2026-08-19-d2b-cutover.md +++ /dev/null @@ -1,5 +0,0 @@ -### Added - -- Add a pure, resumable cutover and scoped-reset contract engine with - canonical previews, single-use consent, digest-bound journals, replay-safe - effects, holds, verification, and rollback boundaries. diff --git a/changelog.d/2026-08-19-spec001-wave7-bazel-parity.md b/changelog.d/2026-08-19-spec001-wave7-bazel-parity.md deleted file mode 100644 index 2c747de473..0000000000 --- a/changelog.d/2026-08-19-spec001-wave7-bazel-parity.md +++ /dev/null @@ -1,4 +0,0 @@ -### Changed - -- Restored Bazel parity for the root Cargo workspace, including cutover - binaries, tests, policy scans, and provider redaction coverage. diff --git a/changelog.d/2026-08-19-spec001-wave7-u6-cutover-rehearsal.md b/changelog.d/2026-08-19-spec001-wave7-u6-cutover-rehearsal.md deleted file mode 100644 index c8dd817633..0000000000 --- a/changelog.d/2026-08-19-spec001-wave7-u6-cutover-rehearsal.md +++ /dev/null @@ -1,6 +0,0 @@ -### Added - -- Add the U6 booted-VM cutover rehearsal, candidate-bound heavy live driver, - safe cutover runbook, and security manual-validation checklist. The live - lane validates recovery and delivery evidence through production validators, - stops at `CutoverSucceeded`, and never performs phase-10 finalization. diff --git a/changelog.d/2026-08-19-u4-cutover-resource-transport.md b/changelog.d/2026-08-19-u4-cutover-resource-transport.md deleted file mode 100644 index 7f887ed40a..0000000000 --- a/changelog.d/2026-08-19-u4-cutover-resource-transport.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed - -- Canonicalize host cutover contract files before Zone transport and emit - preview, apply, reset, and observation responses through the canonical - resource boundary. diff --git a/changelog.d/2026-08-19-u4-cutover-runtime.md b/changelog.d/2026-08-19-u4-cutover-runtime.md deleted file mode 100644 index c8b9513e78..0000000000 --- a/changelog.d/2026-08-19-u4-cutover-runtime.md +++ /dev/null @@ -1,14 +0,0 @@ -### Added - -- Add the unblocked U4 cutover runtime boundary: typed host cutover CLI and - daemon admission, a single-use fd bootstrap capability, an out-of-band - runner with durable journal and OFD lock ownership, lifecycle-authenticated - status and hold/resume socket controls, and the narrow broker launch - operation. Hold and resume fail closed until the privileged audit boundary - returns durable evidence. Register the runner as a non-persistent host tool - and route typed effects and closure activation through the adapted broker - capability boundary and typed host-generation handoff; existing storage, - activation, provider-start, and verification operations are adapted with - durable evidence. Add marker-bound quarantine, separately consented - finalization, and durable-Volume destroy adapters; scoped reset admission - uses a separate scope-bound operation capability and consent. diff --git a/changelog.d/2026-08-21-provider-placement-contracts.md b/changelog.d/2026-08-21-provider-placement-contracts.md new file mode 100644 index 0000000000..153b7016c4 --- /dev/null +++ b/changelog.d/2026-08-21-provider-placement-contracts.md @@ -0,0 +1,5 @@ +### Added + +- Define signed controller scopes, target capabilities, canonical ResourceType + placement anchors, per-target EffectPort requirements, and shared Host/Guest + daemon and broker artifact checks. diff --git a/changelog.d/2026-08-21-resource-assignment-routing.md b/changelog.d/2026-08-21-resource-assignment-routing.md new file mode 100644 index 0000000000..62ae43c363 --- /dev/null +++ b/changelog.d/2026-08-21-resource-assignment-routing.md @@ -0,0 +1,4 @@ +### Changed + +- Fence Provider controller assignments to one resource target and route scoped + Resource API watches and mutations through the existing Zone bus. diff --git a/changelog.d/2026-08-21-v3-guest-u3.md b/changelog.d/2026-08-21-v3-guest-u3.md new file mode 100644 index 0000000000..b72f04fd1b --- /dev/null +++ b/changelog.d/2026-08-21-v3-guest-u3.md @@ -0,0 +1,3 @@ +- Add fixed Host and Guest `d2bd` modes with provider-neutral Guest + ComponentSession enrollment, bounded target admission, and mode-bound + broker effects. diff --git a/changelog.d/2026-08-21-v3-guest-u6.md b/changelog.d/2026-08-21-v3-guest-u6.md new file mode 100644 index 0000000000..527fa23533 --- /dev/null +++ b/changelog.d/2026-08-21-v3-guest-u6.md @@ -0,0 +1,27 @@ +### Added + +- Route Guest Process and EphemeralProcess execution through authenticated + Guest ComponentSession and Guest-profile broker contracts with signed + target, session, boot, assignment, and generation binding. + +### Changed + +- Use typed Resource API services for Guest activation, configuration access, + and readiness instead of legacy Guest control callers. +- Remove the standalone no-bash AST walker test and its executable Bazel and + Make wiring from the source-hygiene graph. +- Restore remote BuildBuddy compilation with local Bazel test execution until + the remote worker image supplies the upstream test wrapper runtime. +- Remove the heavy-gate semaphore, self-reexec guards, and host provisioning; + public integration, performance, smoke, and flake lanes now run their + existing work directly. +- Let BuildBuddy compile Rust tests while the remote profile runs their test + actions through the pinned local project shell. +- Remove obsolete per-target remote-execution exclusions from local-runtime + Rust suites. +- Remove the redundant local Rust suite; package suites now compile every Rust + test through BuildBuddy. +- Limit stable-principal collision checks to hash-derived UIDs so declared + Guest-root activation profiles can share UID 0. +- Capture all Bazel test output in the facade warning gate instead of relying + on non-local BuildBuddy test-log URIs. diff --git a/changelog.d/2026-08-25-cutover-removal.md b/changelog.d/2026-08-25-cutover-removal.md new file mode 100644 index 0000000000..ce480f3660 --- /dev/null +++ b/changelog.d/2026-08-25-cutover-removal.md @@ -0,0 +1,5 @@ +### Removed + +- Removed the unreleased host cutover feature, including its CLI, daemon and + broker protocols, runner, Nix packaging, schemas, tests, fixtures, and + current operator documentation. diff --git a/changelog.d/adr046-u11-broker-profiles.md b/changelog.d/adr046-u11-broker-profiles.md new file mode 100644 index 0000000000..fd0d18b4a9 --- /dev/null +++ b/changelog.d/adr046-u11-broker-profiles.md @@ -0,0 +1,3 @@ +- Rename the privileged broker package and executable to `d2b-broker` and add + fixed Host and Guest profiles with separate socket, state, audit, caller, + and authority bindings. diff --git a/changelog.d/adr046-w0fu6-specs.md b/changelog.d/adr046-w0fu6-specs.md index 3d913acf97..e6c73ae5db 100644 --- a/changelog.d/adr046-w0fu6-specs.md +++ b/changelog.d/adr046-w0fu6-specs.md @@ -48,12 +48,6 @@ same-Zone claimants, and a cross-Zone `bridge` multiplex is rejected fail closed (`external-physical-nic-cross-zone-l2`) so work and personal Zones never share an L2 broadcast domain. -- Rewrote host cutover as in-place adaptation of exactly the three root-visible - units (`d2bd.service`, `d2b-priv-broker.socket`, `d2b-priv-broker.service`): - removed the parallel Zone-runtime unit set and the step that destroyed the - three units, and required an exact-three-units integration assertion matching - the framework host exit criterion. - ### Fixed - Propagated D119 across every spec that still froze retired bundle names, diff --git a/changelog.d/ci-root-rust-toolchain-compat.md b/changelog.d/ci-root-rust-toolchain-compat.md new file mode 100644 index 0000000000..478fd6faab --- /dev/null +++ b/changelog.d/ci-root-rust-toolchain-compat.md @@ -0,0 +1,9 @@ +### Fixed + +- Keep trusted CI compatible with the root Rust toolchain authority while the + protected workflow still reads the former package-local path. +- Remove the retired `rust-local` CI shard, make broker profile tests + runner-UID independent, and use native Bash for Bazel actions on FHS hosts. +- Give cold realized Nix and advisory performance jobs sufficient hosted-runner + time, and keep the async blocking-adapter heartbeat below its backend delay + without assuming sub-5-ms scheduler jitter. diff --git a/changelog.d/feat-spec001-wave7-completion.md b/changelog.d/feat-spec001-wave7-completion.md deleted file mode 100644 index 010159900c..0000000000 --- a/changelog.d/feat-spec001-wave7-completion.md +++ /dev/null @@ -1,24 +0,0 @@ -### Fixed - -- Normalize cutover runner responses through the public contract, keep doctor - available during daemon drain, bind host-generation handoffs to admitted - preview and consent artifact identities, validate handoffs before host drain, - validate both candidate and rollback artifacts through the trusted helper - before host drain, recover ambiguous apply responses from runner state, use - explicit mutating resource transport for cutover writes, and transfer - bounded runner bootstraps without pipe backpressure. -- Return precise artifact-binding failures, release failed launch capabilities, - register cutover runner pidfds for broker reaping, and make live cutover - assertions consume public JSON without forbidden `jq -e` predicates. -- Require runner admission readiness before d2bd reports an apply launch, - clean up registered runners when launch auditing fails, and reject missing - operation ids on every runner-owned continuation command. -- Validate cutover artifact identities with the canonical v3 grammar and - retain runner process identity in the broker capability registry so an - exited runner does not permanently block a retry. -- Permit dead-runner capability rotation during journal adoption and have - repeated apply commands resume through an already admitted runner. -- Refresh CI-owned cutover completions and daemon API artifacts, restore the - cutover policy markers and broker disposition rows, preserve admin and - launcher lifecycle-group membership, and declare the direct Bazel contract - dependency for the vsock framing test. diff --git a/changelog.d/fix-host-cutover-privilege-parity.md b/changelog.d/fix-host-cutover-privilege-parity.md deleted file mode 100644 index e2e15ee38f..0000000000 --- a/changelog.d/fix-host-cutover-privilege-parity.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed - -- Align the host cutover public authorization row across the Nix bundle, Rust - matrix, generated schema, and privileges reference while keeping it - admin-only. diff --git a/changelog.d/fix-nix-host-tool-source-schemas.md b/changelog.d/fix-nix-host-tool-source-schemas.md new file mode 100644 index 0000000000..3af39e9873 --- /dev/null +++ b/changelog.d/fix-nix-host-tool-source-schemas.md @@ -0,0 +1,4 @@ +### Fixed + +- Include v3 provider schemas in isolated Nix fixture sources so profile-capable + host tools compile crates that embed their schemas at build time. diff --git a/changelog.d/phase-v3-guest-u4.md b/changelog.d/phase-v3-guest-u4.md new file mode 100644 index 0000000000..9fba32c581 --- /dev/null +++ b/changelog.d/phase-v3-guest-u4.md @@ -0,0 +1,4 @@ +### Fixed + +- Process Provider controllers now validate launch tickets and exact process identity before readiness, adoption, and stop effects on Host and Guest targets. +- Regenerated the signed acceptance Provider manifest for the target-local controller contract. diff --git a/changelog.d/phase-v3-guest-u7.md b/changelog.d/phase-v3-guest-u7.md new file mode 100644 index 0000000000..0e958dc2ac --- /dev/null +++ b/changelog.d/phase-v3-guest-u7.md @@ -0,0 +1,19 @@ +### Changed + +- Moved semantic audio, USBIP, security-key, and telemetry Binding realization + onto deterministic Provider-owned child intents reconciled through Core. + Child creation, repair, and teardown preserve explicit Binding ownership, + target placement, optimistic identity fencing, and Endpoint-before-Process + deletion ordering without creating consumer Bindings from Services. +- AudioBinding status now persists channel grants, levels and gains, + arbitration, enforcement posture, and the last applied host/guest path + alongside the observed Service and realization references. + +### Fixed + +- Admit Binding Endpoint children by validating their reserved `providerRef` + with the canonical full `ResourceSpec`. +- Run the AudioBinding Guest worker as a system-domain Process without an + unbound `userRef`, allowing the systemd Process Provider to converge it. +- Derive Binding child deletion readiness only after deletion is requested, so + live children with no finalizers remain valid during relist. diff --git a/changelog.d/phase-v3-guest-u8.md b/changelog.d/phase-v3-guest-u8.md new file mode 100644 index 0000000000..f27f26296b --- /dev/null +++ b/changelog.d/phase-v3-guest-u8.md @@ -0,0 +1,19 @@ +### Changed + +- Move Wayland display proxy lifecycle under durable Host and Guest Process + resources managed by `d2bd`, preserving signed execution references, + restart adoption, ordered deletion, and typed clipboard and notification + streams. +- Keep Host process Providers from treating Guest execution references as + local launches until authenticated remote process routing is available. +- Persist display Endpoint children through the production Redb resource plane, + including finalizer-gated deletion and restart reopening. +- Bind display mutation idempotency keys to their request payload or exact + resource revision, and drain Process children before deleting Endpoints. +- Remove the obsolete display-specific broker routing and Guest systemd + fallback; production display execution now uses the signed Process path. +- Reconcile the committed WaylandSession during trusted graphics VM start so + the Host proxy Process is resource-backed and launched before GPU readiness; + missing display sessions fail closed without a direct-spawn fallback. +- Keep QEMU-media Wayland proxies in the legacy VM-start DAG by distinguishing + their existing process identity from trusted graphics resource nodes. diff --git a/changelog.d/process-shell-component-session.md b/changelog.d/process-shell-component-session.md new file mode 100644 index 0000000000..03173cb4f1 --- /dev/null +++ b/changelog.d/process-shell-component-session.md @@ -0,0 +1,11 @@ +### Changed + +- Route guest execution and persistent shell lifecycle through Process-family resource intents and authenticated ComponentSession named streams while retaining bounded authorization, retention, and disconnect handling; unavailable target-session routes fail closed without a legacy fallback. +- Demultiplex concurrent named-stream responses, cancel/reset on owner disconnect, and use one correlated JSON frame codec across CLI, ResourceClient, Providers, and daemon shell owners. +- Reject the retired public `exec`/guest shell bridge in production composition and record target-local supervisor Process ownership for ShellSession resources. +- Resolve enrolled target-local ComponentSessions for real Process and ShellSession routes, and persist shell supervisor Process ownership through the daemon Resource API with exact owner and finalizer fencing. + +### Fixed + +- Preserve the development-shell PATH for local Bazel test runners without + overriding the worker-standard action PATH used by remote profiles. diff --git a/changelog.d/refactor-bazel-host-integration-binaries.md b/changelog.d/refactor-bazel-host-integration-binaries.md index 3479d06826..5731e36a5e 100644 --- a/changelog.d/refactor-bazel-host-integration-binaries.md +++ b/changelog.d/refactor-bazel-host-integration-binaries.md @@ -1,6 +1,6 @@ ### Changed -- `make test-host-integration` now builds the fixed nine host tools with local +- `make test-host-integration` now builds the fixed eight host tools with local Bazel, injects them into the selected NixOS VM checks, and uploads successful dependency closures to a configured Attic cache without caching the VM test results themselves. When Attic or its configuration is unavailable, the lane diff --git a/changelog.d/spec001-wave7-cutover-review-fixes.md b/changelog.d/spec001-wave7-cutover-review-fixes.md deleted file mode 100644 index 841fafb8e4..0000000000 --- a/changelog.d/spec001-wave7-cutover-review-fixes.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed - -- Make cutover runners adopt and resume durable journals safely, use broker-owned - admission and verification observations, bound broker I/O, and preserve - retryable or restore-required failure state across crashes. diff --git a/changelog.d/spec001-wave7-u2-security-streamline.md b/changelog.d/spec001-wave7-u2-security-streamline.md index 1532d54415..03b1b0dfc7 100644 --- a/changelog.d/spec001-wave7-u2-security-streamline.md +++ b/changelog.d/spec001-wave7-u2-security-streamline.md @@ -2,7 +2,6 @@ - Enforce Provider EffectPort, specification vocabulary, test-taxonomy, deterministic-time, and current threat-matrix policies. - Extend interaction canary coverage across clipboard, terminal, notifications, and security-key CTAP surfaces. - Add zero-secret Credential canaries for merged Entra, managed identity, and Secret Service configuration, placement, session, lease, ambiguous-completion, recovery, audit, telemetry, Debug, and support surfaces. -- Extend the security matrix across the U4 cutover/reset contracts while keeping live drain and effect execution explicitly pending on the unfinished U4 runtime boundary. ### Fixed - Redact CTAPHID report bytes from frontend Debug output while retaining privileged audit fail-closed and quarantine evidence. diff --git a/changelog.d/v3-guest-u10-clean-break.md b/changelog.d/v3-guest-u10-clean-break.md new file mode 100644 index 0000000000..f65f356eb1 --- /dev/null +++ b/changelog.d/v3-guest-u10-clean-break.md @@ -0,0 +1,27 @@ +### Removed + +- Removed production reachability for the retired guest-control exec and + config wires, direct exec owner, and guest-control shell and activation + adapters. Process resources, ConfigNixos, and ComponentSession named + streams now own those paths. +- Removed the standalone guest daemon, legacy guest protocol bindings, + token-share broker operation, obsolete SSH readiness role, and retired + package-policy inputs. Old peers fail closed at the ComponentSession + boundary. +- Removed the Wayland proxy's compatibility host-terminal child launcher; + desktop terminal processes remain owned by their signed Process or + companion. + +### Fixed + +- Require a configured non-root workload user for persistent Guest shell + service wiring and keep ComponentSession evaluation aligned with the active + `d2b.sshUser` field. +- Keep the editable Guest configuration in its dedicated workload-owned + directory while granting `d2bd` group access to serve it. +- Fail non-qemu USBIP attach, detach, and VM-start reconciliation closed with + typed `runtime-capability-unsupported` when the target-local + `usbip-guest-proxy` Process and ComponentSession path is unavailable. Host + bind, proxy reconciliation, and claim release are no longer reported or + performed as successful USB attachment work; VM-stop cleanup preserves the + claim until Guest detach is available. diff --git a/changelog.d/v3-guest-u9-component-session.md b/changelog.d/v3-guest-u9-component-session.md new file mode 100644 index 0000000000..0e79b00d86 --- /dev/null +++ b/changelog.d/v3-guest-u9-component-session.md @@ -0,0 +1,5 @@ +- Cut ordinary Guest packaging over to the mode-bound `d2bd guest` and + `d2b-broker guest` ComponentSession shape, including shared static artifact + and Guest workspace coverage. +- Keep gateway parent-Guest and child-Zone daemon and broker instances on + separate mode-bound sockets and state roots. diff --git a/completions/d2b.bash b/completions/d2b.bash index 266cd354e8..809e80a21b 100644 --- a/completions/d2b.bash +++ b/completions/d2b.bash @@ -451,9 +451,6 @@ _d2b() { d2b__subcmd__host,check) cmd="d2b__subcmd__host__subcmd__check" ;; - d2b__subcmd__host,cutover) - cmd="d2b__subcmd__host__subcmd__cutover" - ;; d2b__subcmd__host,destroy) cmd="d2b__subcmd__host__subcmd__destroy" ;; @@ -481,36 +478,6 @@ _d2b() { d2b__subcmd__host,validate) cmd="d2b__subcmd__host__subcmd__validate" ;; - d2b__subcmd__host__subcmd__cutover,apply) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__apply" - ;; - d2b__subcmd__host__subcmd__cutover,doctor) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__doctor" - ;; - d2b__subcmd__host__subcmd__cutover,finalize) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__finalize" - ;; - d2b__subcmd__host__subcmd__cutover,hold) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__hold" - ;; - d2b__subcmd__host__subcmd__cutover,preview) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__preview" - ;; - d2b__subcmd__host__subcmd__cutover,reset) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__reset" - ;; - d2b__subcmd__host__subcmd__cutover,resume) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__resume" - ;; - d2b__subcmd__host__subcmd__cutover,rollback) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__rollback" - ;; - d2b__subcmd__host__subcmd__cutover,status) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__status" - ;; - d2b__subcmd__host__subcmd__cutover,verify) - cmd="d2b__subcmd__host__subcmd__cutover__subcmd__verify" - ;; d2b__subcmd__import,create) cmd="d2b__subcmd__import__subcmd__create" ;; @@ -4073,7 +4040,7 @@ _d2b() { return 0 ;; d2b__subcmd__host) - opts="-h --zone --json --human --deadline --no-deadline --help get list status check prepare destroy doctor install reconcile validate cutover" + opts="-h --zone --json --human --deadline --no-deadline --help get list status check prepare destroy doctor install reconcile validate" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -4116,972 +4083,6 @@ _d2b() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - d2b__subcmd__host__subcmd__cutover) - opts="-h --zone --json --human --deadline --no-deadline --help preview status hold resume apply rollback verify doctor finalize reset" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__apply) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__doctor) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__finalize) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__hold) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__preview) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__reset) - opts="-h --operation-id --candidate-id --revision-plan-id --preview-digest --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --scope --target --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --scope) - COMPREPLY=($(compgen -W "zone provider guest" -- "${cur}")) - return 0 - ;; - --target) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__resume) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__rollback) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__status) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - d2b__subcmd__host__subcmd__cutover__subcmd__verify) - opts="-h --operation-id --candidate-id --revision-plan-id --system-artifact-id --source-system-artifact-id --preview-digest --recovery-digest --operator-id --consent-digest --consent-file --destructive-consent-digest --destructive-consent-file --destroy-durable-volumes --recovery-attestation-file --handoff-file --finalization-file --verification-file --host-digest --fresh-consent-digest --reason --zone --json --human --deadline --no-deadline --help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --operation-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --candidate-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --revision-plan-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --source-system-artifact-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --preview-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --operator-id) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --destructive-consent-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --recovery-attestation-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --handoff-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --finalization-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --verification-file) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --host-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --fresh-consent-digest) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --reason) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --zone) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --deadline) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; d2b__subcmd__host__subcmd__destroy) opts="-h --dry-run --apply --zone --json --human --deadline --no-deadline --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then diff --git a/completions/d2b.fish b/completions/d2b.fish index b7cb8c736e..a4f48f4892 100644 --- a/completions/d2b.fish +++ b/completions/d2b.fish @@ -147,23 +147,22 @@ complete -c d2b -n "__fish_d2b_using_subcommand reconcile" -l json -d 'Emit the complete -c d2b -n "__fish_d2b_using_subcommand reconcile" -l human -d 'Force human-readable terminal output' complete -c d2b -n "__fish_d2b_using_subcommand reconcile" -l no-deadline -d 'Suppress the command default deadline' complete -c d2b -n "__fish_d2b_using_subcommand reconcile" -s h -l help -d 'Print help' -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -l zone -d 'Address a declared Zone. Without this flag the nearest local runtime is selected' -r -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -l deadline -d 'Bound all Zone requests and streams' -r -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -l json -d 'Emit the stable JSON envelope' -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -l human -d 'Force human-readable terminal output' -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -l no-deadline -d 'Suppress the command default deadline' -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -s h -l help -d 'Print help' -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "get" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "list" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "status" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "check" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "prepare" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "destroy" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "doctor" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "install" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "reconcile" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "validate" -complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate cutover" -f -a "cutover" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -l zone -d 'Address a declared Zone. Without this flag the nearest local runtime is selected' -r +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -l deadline -d 'Bound all Zone requests and streams' -r +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -l json -d 'Emit the stable JSON envelope' +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -l human -d 'Force human-readable terminal output' +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -l no-deadline -d 'Suppress the command default deadline' +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -s h -l help -d 'Print help' +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "get" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "list" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "status" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "check" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "prepare" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "destroy" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "doctor" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "install" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "reconcile" +complete -c d2b -n "__fish_d2b_using_subcommand host; and not __fish_seen_subcommand_from get list status check prepare destroy doctor install reconcile validate" -f -a "validate" complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from get" -l zone -d 'Address a declared Zone. Without this flag the nearest local runtime is selected' -r complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from get" -l deadline -d 'Bound all Zone requests and streams' -r complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from get" -l json -d 'Emit the stable JSON envelope' @@ -253,22 +252,6 @@ complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from validate" -l human -d 'Force human-readable terminal output' complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from validate" -l no-deadline -d 'Suppress the command default deadline' complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from validate" -s h -l help -d 'Print help' -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -l zone -d 'Address a declared Zone. Without this flag the nearest local runtime is selected' -r -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -l deadline -d 'Bound all Zone requests and streams' -r -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -l json -d 'Emit the stable JSON envelope' -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -l human -d 'Force human-readable terminal output' -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -l no-deadline -d 'Suppress the command default deadline' -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -s h -l help -d 'Print help' -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "preview" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "status" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "hold" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "resume" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "apply" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "rollback" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "verify" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "doctor" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "finalize" -complete -c d2b -n "__fish_d2b_using_subcommand host; and __fish_seen_subcommand_from cutover" -f -a "reset" complete -c d2b -n "__fish_d2b_using_subcommand guest; and not __fish_seen_subcommand_from get list status start stop restart create update-spec delete console" -l zone -d 'Address a declared Zone. Without this flag the nearest local runtime is selected' -r complete -c d2b -n "__fish_d2b_using_subcommand guest; and not __fish_seen_subcommand_from get list status start stop restart create update-spec delete console" -l deadline -d 'Bound all Zone requests and streams' -r complete -c d2b -n "__fish_d2b_using_subcommand guest; and not __fish_seen_subcommand_from get list status start stop restart create update-spec delete console" -l json -d 'Emit the stable JSON envelope' diff --git a/completions/d2b.zsh b/completions/d2b.zsh index d94045351e..26ff5f675f 100644 --- a/completions/d2b.zsh +++ b/completions/d2b.zsh @@ -327,330 +327,6 @@ _arguments "${_arguments_options[@]}" : \ '-h[Print help]' \ '--help[Print help]' \ && ret=0 -;; -(cutover) -_arguments "${_arguments_options[@]}" : \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -":: :_d2b__subcmd__host__subcmd__cutover_commands" \ -"*::: :->cutover" \ -&& ret=0 - - case $state in - (cutover) - words=($line[1] "${words[@]}") - (( CURRENT += 1 )) - curcontext="${curcontext%:*:*}:d2b-host-cutover-command-$line[1]:" - case $line[1] in - (preview) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(status) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(hold) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(resume) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(apply) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(rollback) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(verify) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(doctor) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(finalize) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--system-artifact-id=[]:SYSTEM_ARTIFACT_ID:_default' \ -'--source-system-artifact-id=[]:SOURCE_SYSTEM_ARTIFACT_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--recovery-digest=[]:RECOVERY_DIGEST:_default' \ -'--operator-id=[]:OPERATOR_ID:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--recovery-attestation-file=[]:RECOVERY_ATTESTATION_FILE:_files' \ -'--handoff-file=[]:HANDOFF_FILE:_files' \ -'--finalization-file=[]:FINALIZATION_FILE:_files' \ -'--verification-file=[]:VERIFICATION_FILE:_files' \ -'--host-digest=[]:HOST_DIGEST:_default' \ -'--fresh-consent-digest=[]:FRESH_CONSENT_DIGEST:_default' \ -'--reason=[]:REASON:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(reset) -_arguments "${_arguments_options[@]}" : \ -'--operation-id=[]:OPERATION_ID:_default' \ -'--candidate-id=[]:CANDIDATE_ID:_default' \ -'--revision-plan-id=[]:REVISION_PLAN_ID:_default' \ -'--preview-digest=[]:PREVIEW_DIGEST:_default' \ -'--consent-digest=[]:CONSENT_DIGEST:_default' \ -'--consent-file=[]:CONSENT_FILE:_files' \ -'--destructive-consent-digest=[]:DESTRUCTIVE_CONSENT_DIGEST:_default' \ -'--destructive-consent-file=[]:DESTRUCTIVE_CONSENT_FILE:_files' \ -'--scope=[]:SCOPE:(zone provider guest)' \ -'--target=[]:TARGET:_default' \ -'--zone=[Address a declared Zone. Without this flag the nearest local runtime is selected]:ZONE:_default' \ -'--deadline=[Bound all Zone requests and streams]:DURATION:_default' \ -'--destroy-durable-volumes[]' \ -'(--human)--json[Emit the stable JSON envelope]' \ -'(--json)--human[Force human-readable terminal output]' \ -'(--deadline)--no-deadline[Suppress the command default deadline]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; - esac - ;; -esac ;; esac ;; @@ -5349,7 +5025,6 @@ _d2b__subcmd__host_commands() { 'install:' \ 'reconcile:' \ 'validate:' \ -'cutover:' \ ) _describe -t commands 'd2b host commands' commands "$@" } @@ -5358,72 +5033,6 @@ _d2b__subcmd__host__subcmd__check_commands() { local commands; commands=() _describe -t commands 'd2b host check commands' commands "$@" } -(( $+functions[_d2b__subcmd__host__subcmd__cutover_commands] )) || -_d2b__subcmd__host__subcmd__cutover_commands() { - local commands; commands=( -'preview:' \ -'status:' \ -'hold:' \ -'resume:' \ -'apply:' \ -'rollback:' \ -'verify:' \ -'doctor:' \ -'finalize:' \ -'reset:' \ - ) - _describe -t commands 'd2b host cutover commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__apply_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__apply_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover apply commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__doctor_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__doctor_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover doctor commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__finalize_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__finalize_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover finalize commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__hold_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__hold_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover hold commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__preview_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__preview_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover preview commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__reset_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__reset_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover reset commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__resume_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__resume_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover resume commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__rollback_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__rollback_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover rollback commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__status_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__status_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover status commands' commands "$@" -} -(( $+functions[_d2b__subcmd__host__subcmd__cutover__subcmd__verify_commands] )) || -_d2b__subcmd__host__subcmd__cutover__subcmd__verify_commands() { - local commands; commands=() - _describe -t commands 'd2b host cutover verify commands' commands "$@" -} (( $+functions[_d2b__subcmd__host__subcmd__destroy_commands] )) || _d2b__subcmd__host__subcmd__destroy_commands() { local commands; commands=() diff --git a/docs/README.md b/docs/README.md index e30801c6b0..7a9b790417 100644 --- a/docs/README.md +++ b/docs/README.md @@ -106,9 +106,6 @@ The contracts. Stable interfaces a consumer can depend on. - [`reference/constellation-observability.md`](./reference/constellation-observability.md) - bounded `d2b op inspect`, TraceContext propagation, degraded partial results, and telemetry redaction/cardinality constraints. -- [`reference/guest-control-persistent-shell.md`](./reference/guest-control-persistent-shell.md) - - guest-control shell RPCs, terminal-generic streaming, shell states, - close causes, and redaction contracts. - [`reference/remote-full-host-nodes.md`](./reference/remote-full-host-nodes.md) - gateway-managed remote d2b hosts: registration, capability gating, operation routing, idempotency, and the non-tunneling boundary. diff --git a/docs/adr/0028-guest-control-plane-over-vsock.md b/docs/adr/0028-guest-control-plane-over-vsock.md index 1d19b34868..77d4223151 100644 --- a/docs/adr/0028-guest-control-plane-over-vsock.md +++ b/docs/adr/0028-guest-control-plane-over-vsock.md @@ -1,26 +1,17 @@ # ADR 0028: Guest control plane over virtio-vsock -- Status: Accepted +- Status: Superseded by ADR 0043 and U10 ComponentSession clean break - Date: 2026-06-08 - Related: ADR 0010 (wire protocol and typed errors), ADR 0015 (daemon-only clean break), ADR 0017 (no bash fallbacks), ADR 0018 (microvm.nix removal), ADR 0024 (in-VM guest config sync) -> **Update (W16) - current shipped reality.** The guest-control plane -> described here has landed: `d2b-guestd` serves `Hello`/`Health`/ -> `Capabilities`, the bounded `ReadGuestFile` read, and the exec -> lifecycle RPCs over the authenticated vsock channel. `d2b config -> sync` reads the guest config working copy over `ReadGuestFile` (no -> `ssh cat`) and **fails closed** on a VM whose running generation does -> not declare the guest-control transport - the SSH compatibility path -> sketched below is **not yet wired** into the command. `d2b vm -> konsole` runs `d2b vm exec -it` over guest-control (no SSH), and -> the admin-only `d2b vm exec` verb shipped alongside it. The DAG -> readiness node is `guest-control-health`, not `guest-ssh-readiness`. -> Per-VM SSH keys are retained only for the remaining compatibility -> surfaces (notably `usb attach --apply`). There is no separate -> guest-control field on `d2b status` yet. The original decision -> text below is preserved as the historical record. +> **Historical record.** U10 removed the standalone Guest daemon, this +> feature-specific ttRPC contract, its generated bindings, token-share +> broker operation, and SSH compatibility path. Current Guest control uses +> the enrolled ComponentSession and signed target-local Processes described +> by ADR 0043. The original decision text below is retained only to explain +> the superseded design and migration history; it is not a current contract. ## Context @@ -314,8 +305,8 @@ when the interactive path is usable (the PTY spawner is wired, which requires the exec-runner helper to be present). The full interactive contract - mode matrix, helper-exec handshake, merged -output, VEOF close, resize/signal ordering, and teardown - is specified in the -[interactive TTY exec reference](../reference/guest-control-exec-interactive-tty.md). +output, VEOF close, resize/signal ordering, and teardown - is now covered by +the current [CLI contract](../reference/cli-contract.md#exec). ## Feasibility gate @@ -482,7 +473,7 @@ for failing a must-pass row. enums, never guest-derived free-form text or transport/socket paths. The closed W0 enum set and allowed state/reason/remediation mappings live in - [the chunked stdio reference](../reference/guest-control-exec-io-chunked-stdio.md#health-status-model). + [the current CLI contract](../reference/cli-contract.md#exec). W0 reserves these states for the implementing schema: `healthy`, `degraded`, `unavailable-old-generation`, `listener-absent`, `transport-unreachable`, `auth-failed`, `protocol-mismatch`, and diff --git a/docs/adr/0029-framework-ssh-to-typed-guest-rpc.md b/docs/adr/0029-framework-ssh-to-typed-guest-rpc.md index 28a85dd71b..37a23548b9 100644 --- a/docs/adr/0029-framework-ssh-to-typed-guest-rpc.md +++ b/docs/adr/0029-framework-ssh-to-typed-guest-rpc.md @@ -1,23 +1,16 @@ # ADR 0029: Migrate framework SSH operations to typed guest-control RPCs -- Status: Accepted (Unreleased) +- Status: Superseded by U10 ComponentSession clean break - Date: 2026-06-09 - Related: ADR 0015 (daemon-only clean break), ADR 0024 (in-VM guest config editing, sync, and containment), ADR 0028 (guest-control plane over vsock) -> **Update (W16) - current shipped reality.** This migration landed. -> Framework readiness gates on the fail-closed `guest-control-health` -> DAG node (authenticated guest-control `Health`), not the raw TCP-22 -> `guest-ssh-readiness` node. `config sync` reads the guest config -> working copy over the typed bounded `ReadGuestFile` RPC; on a VM whose -> running generation does not declare the guest-control transport it -> **fails closed** - the operator SSH compatibility transport described -> below is **not yet wired** into the command. `d2b vm konsole` now -> runs over guest-control (no SSH) via the shipped admin-only `d2b -> vm exec`. Per-VM SSH keys remain only for the surviving compatibility -> surfaces (notably `usb attach --apply`). The original decision text -> below is preserved as the historical record. +> **Historical record.** U10 removed the feature-specific guest-control RPC, +> standalone Guest daemon, and SSH compatibility route described here. Current +> readiness and execution use the enrolled ComponentSession and signed +> target-local Processes. The original decision text below is retained only as +> migration history. ## Context diff --git a/docs/adr/0030-guest-exec-as-workload-user.md b/docs/adr/0030-guest-exec-as-workload-user.md index 50196e5ef9..77b3418860 100644 --- a/docs/adr/0030-guest-exec-as-workload-user.md +++ b/docs/adr/0030-guest-exec-as-workload-user.md @@ -1,11 +1,16 @@ # ADR 0030: Guest exec runs as the workload user in a PAM login session -- Status: Accepted (Unreleased) +- Status: Accepted (historical implementation details superseded by U10) - Date: 2026-06-13 - Related: ADR 0028 (guest-control plane over vsock), ADR 0029 (migrate framework SSH operations to typed guest-control RPCs), ADR 0015 (daemon-only clean break) +> **Current implementation note.** The workload-user and PAM-session invariant +> remains, but U10 routes it through signed target-local Process resources and +> ComponentSession rather than the standalone Guest daemon or guest-control +> RPC described below. + ## Context The guest-control plane (ADR 0028) shipped guest exec as **root-only**: diff --git a/docs/adr/0032-d2b-v2-constellation-control-plane.md b/docs/adr/0032-d2b-v2-constellation-control-plane.md index 12b2d2005c..ac3f6d6c59 100644 --- a/docs/adr/0032-d2b-v2-constellation-control-plane.md +++ b/docs/adr/0032-d2b-v2-constellation-control-plane.md @@ -2793,7 +2793,7 @@ Negative/trade-offs: - [Daemon API reference](../reference/daemon-api.md) - [Tracing contract](../reference/tracing-contract.md) - [Observability component reference](../reference/components-observability.md) -- [Guest-control exec I/O reference](../reference/guest-control-exec-io-chunked-stdio.md) +- [ComponentSession exec contract](../reference/cli-contract.md#exec) - [Graphics component reference](../reference/components-graphics.md) - [Audio component reference](../reference/components-audio.md) - [USBIP component reference](../reference/components-usbip.md) diff --git a/docs/adr/0038-persistent-guest-shell-sessions.md b/docs/adr/0038-persistent-guest-shell-sessions.md index 0fdd3c3d40..de18f08ab4 100644 --- a/docs/adr/0038-persistent-guest-shell-sessions.md +++ b/docs/adr/0038-persistent-guest-shell-sessions.md @@ -1,6 +1,6 @@ # ADR 0038: Persistent named guest shell sessions -- Status: Accepted +- Status: Accepted (historical implementation details superseded by U10) - Date: 2026-06-21 - Related: ADR 0015 (daemon-only clean break), ADR 0026 (native SigNoz observability backend), ADR 0028 (guest control plane over virtio-vsock), @@ -11,6 +11,12 @@ (efficiency and simplification roadmap), ADR 0037 (local hypervisor runtime seam) +> **Current implementation note.** The shell lifecycle remains a +> `ShellSession` resource with authenticated ComponentSession named streams, +> but the standalone Guest daemon and guest-control RPCs described below were +> removed by U10. Target-local signed Processes and the Guest-profile broker +> now own execution and lifecycle; references to `guestd` below are historical. + ## Context `d2b vm exec -it -- ` gives operators a workload-user terminal diff --git a/docs/adr/0039-constellation-persistent-shell-routing.md b/docs/adr/0039-constellation-persistent-shell-routing.md index a671d0f3c7..888609f568 100644 --- a/docs/adr/0039-constellation-persistent-shell-routing.md +++ b/docs/adr/0039-constellation-persistent-shell-routing.md @@ -8,6 +8,11 @@ exec), ADR 0032 (d2b v2 constellation control plane), ADR 0037 (local hypervisor runtime seam), ADR 0038 (persistent named guest shell sessions) +> **Current implementation note (U10).** Persistent shell routing remains +> semantic and stream-scoped, but the old Guest daemon and guest-control +> terminal substrate are removed. Local Guest shells now use ComponentSession +> and signed target-local Process workers. + ## Context ADR 0032 defines constellation routing between local hosts, gateway guests, diff --git a/docs/adr/0041-console-and-audio-controls.md b/docs/adr/0041-console-and-audio-controls.md index 8b3c03afcb..b68c08f4a9 100644 --- a/docs/adr/0041-console-and-audio-controls.md +++ b/docs/adr/0041-console-and-audio-controls.md @@ -9,6 +9,11 @@ hypervisor runtime seam), ADR 0039 (constellation persistent shell routing) +> **Current implementation note (U10).** Guest-side console, audio, and +> terminal effects are owned by signed target-local Processes and typed +> ComponentSession services. The standalone Guest daemon and guest-control +> RPCs named in the baseline context are no longer shipped. + ## Context The Rust CLI already exposes native `d2b console ` and diff --git a/docs/adr/0043-realm-native-control-plane.md b/docs/adr/0043-realm-native-control-plane.md index aad0f0c740..5eb99d2946 100644 --- a/docs/adr/0043-realm-native-control-plane.md +++ b/docs/adr/0043-realm-native-control-plane.md @@ -8,7 +8,7 @@ (wire protocol and typed errors), [ADR 0015](0015-daemon-only-clean-break.md) (daemon-only clean break), [ADR 0025](0025-wayland-proxy-host-jailed-role.md) (host-jailed Wayland filter proxy role), [ADR 0028](0028-guest-control-plane-over-vsock.md) - (guest control plane over virtio-vsock), [ADR 0034](0034-storage-lifecycle-restart-and-synchronization.md) + (ComponentSession control plane over virtio-vsock), [ADR 0034](0034-storage-lifecycle-restart-and-synchronization.md) (storage lifecycle, restart adoption, and synchronization), [ADR 0037](0037-local-hypervisor-runtime-seam.md) (local hypervisor runtime seam), [ADR 0038](0038-persistent-guest-shell-sessions.md) (persistent named guest shell sessions), [ADR 0039](0039-constellation-persistent-shell-routing.md) @@ -19,14 +19,14 @@ D2b's local desktop VM experience is strong: a local `d2b` CLI talks to a local `d2bd`, `d2bd` owns the VM lifecycle DAG, privileged host mutation -flows through `d2b-priv-broker`, guest control is typed, and Wayland +flows through `d2b-priv-broker`, ComponentSession control is typed, and Wayland windows run through d2b-owned local virtualization surfaces. The constellation/realm model is less coherent. [ADR 0032](0032-d2b-v2-constellation-control-plane.md) correctly introduced semantic daemon-to-daemon operations, capability negotiation, remote full-host nodes, provider adapters, and relay-agnostic transport boundaries. It also intentionally avoided raw broker, daemon, -guest-control, SSH, and generic network tunnels. Those invariants remain sound. +component-session, SSH, and generic network tunnels. Those invariants remain sound. The weak point is the ownership model. [ADR 0032](0032-d2b-v2-constellation-control-plane.md) treats realms as an entrypoint table in front of a host daemon, with @@ -266,7 +266,7 @@ The same realm contract can be implemented in different placements: | --- | --- | --- | | `host-local` | Isolated service on the physical host | May own local VM/provider lifecycle through that realm's broker. | | `gateway-vm` | Dedicated local d2b VM | Owns realm policy/credentials inside a microVM boundary. | -| `cloud-full-host` | Cloud VM running full d2b | Owns its local broker, runtimes, and guest-control stack. | +| `cloud-full-host` | Cloud VM running full d2b | Owns its local broker, runtimes, and ComponentSession stack. | | `provider-controller` | Provider-supported environment | Implements the standard d2b realm protocol with a limited capability set. | | `provider-agent` | Inside or adjacent to a managed sandbox | Implements workload/stream subsets when a full `d2bd` cannot run. | @@ -546,7 +546,7 @@ local-root The protocol routes semantic d2b operation and stream frames. It does not create a flat L3/L4 overlay, VPN, raw port-forward default, SSH fallback, raw -guest-control tunnel, raw broker tunnel, pidfd passing, or generic socket +component-session tunnel, raw broker tunnel, pidfd passing, or generic socket proxy. Every routed operation carries a bounded correlation id aligned with W3C Trace @@ -623,7 +623,7 @@ The architecture should pursue local d2b parity where feasible: | --- | --- | | Lifecycle | Core capability for realms that own workload lifecycle. | | Exec | Core typed operation; provider API exec is allowed only as a capability-scoped implementation. | -| Persistent shell | Core where a guestd-compatible agent or full d2bd can provide [ADR 0038](0038-persistent-guest-shell-sessions.md) semantics. | +| Persistent shell | Core where a ComponentSession-capable provider agent or full d2bd can provide [ADR 0038](0038-persistent-guest-shell-sessions.md) semantics. | | Logs | Core bounded stream/summary capability. | | Wayland/display | Core capability for desktop use, implemented through d2b-owned proxy/stream surfaces. | | Clipboard | Capability-gated and must preserve [ADR 0042](0042-d2b-clipboard-authority-and-picker-split.md) picker/clipd authority. | @@ -711,7 +711,7 @@ daemon namespace. An Azure VM realm type provisions or adopts a VM capable of running d2b. The provider may create the infrastructure and inject bootstrap material, but once the VM boots, the cloud host runs its own realm controller and broker. Local -effects are re-originated inside that cloud host's d2bd/broker/guest-control +effects are re-originated inside that cloud host's d2bd/broker/component-session stack. ### Provider-managed sandboxes @@ -719,7 +719,7 @@ stack. An ACA-like provider may not have KVM, systemd, a broker, cgroups, or vsock. It can still participate when it implements the standard d2b semantic contract subset and advertises only the capabilities it can support. A -persistent-shell-capable sandbox must run a guestd-compatible d2b agent; a +persistent-shell-capable sandbox must run a ComponentSession-capable provider agent; a provider-native one-shot command API is not a persistent shell. Existing ACA sandbox support migrates into this model. Old ACA sandbox @@ -973,7 +973,7 @@ test it: and runtime metadata into the selected realm. Local Cloud Hypervisor VMs are part of this required migration. Their disks, -per-VM state, guest-control identity, display/Wayland proxy identity, audit +per-VM state, component-session identity, display/Wayland proxy identity, audit pointers, and runtime metadata move under the owning realm while preserving the local fast path for default-realm use. @@ -1000,7 +1000,7 @@ The following [ADR 0032](0032-d2b-v2-constellation-control-plane.md) decisions remain valid and should be carried forward: - semantic operation/stream frames; -- no raw broker/daemon/guest-control tunneling; +- no raw broker/daemon/component-session tunneling; - relay is untrusted reachability; - capability negotiation and typed denial; - idempotency for mutating operations; diff --git a/docs/adr/guest-control-feasibility-dossier.md b/docs/adr/guest-control-feasibility-dossier.md index 8958e8b859..c57f7ead6b 100644 --- a/docs/adr/guest-control-feasibility-dossier.md +++ b/docs/adr/guest-control-feasibility-dossier.md @@ -4,17 +4,12 @@ This dossier records the W0 evidence required by [ADR 0028](../adr/0028-guest-control-plane-over-vsock.md). It is the review input for locking the guest-control IPC direction. -> **Update (W16) - exploratory/historical.** This is W0 feasibility -> evidence, not a description of the shipped surface. The guest-control -> plane has since landed: `d2b-guestd` serves the unary APIs and the -> chunked-stdio exec RPCs over the authenticated vsock channel, `config -> sync` reads over `ReadGuestFile` and **fails closed** on old -> generations (the `ssh-compat` transport in the compatibility-matrix -> rows below is **not yet wired**), and `vm konsole` / `vm exec` run over -> guest-control with no SSH. Per-VM SSH keys are retained only for the -> remaining compatibility surfaces (notably `usb attach --apply`). Read -> the rows below as the original feasibility analysis, not current -> behaviour. +> **Update (U10) - exploratory/historical.** This is W0 feasibility evidence, +> not a description of the current surface. U10 removed the standalone Guest +> daemon, guest-control protobuf/ttRPC APIs, chunked-stdio path, and SSH +> compatibility transport. Current Guest behavior uses ComponentSession and +> signed target-local Processes. Read the rows below as the original +> feasibility analysis, not current behavior. ## Summary recommendation diff --git a/docs/contributing/architecture.md b/docs/contributing/architecture.md index 016f1492b7..7b87f8b8b6 100644 --- a/docs/contributing/architecture.md +++ b/docs/contributing/architecture.md @@ -18,8 +18,8 @@ broker's `SpawnRunner` / `OpenPidfd` ops. | Resource | Pattern | | --------------------------------------- | -------------------------------------- | | Public daemon (supervisor) | `d2bd.service` | -| Privileged broker socket | `d2b-priv-broker.socket` | -| Privileged broker service | `d2b-priv-broker.service` | +| Privileged broker socket | `d2b-broker.socket` | +| Privileged broker service | `d2b-broker.service` | | Lifecycle permission group | `d2b` (singleton) | VM names are validated at eval time: @@ -110,7 +110,7 @@ per-VM units are emitted): descendants; the restarted daemon re-adopts existing runners. The existing guarded `ExecStop` host-shutdown hook remains the all-VM teardown path and runs only when the system manager is stopping. -- `d2b-priv-broker.service` is socket-activated. It reloads the +- `d2b-broker.service` is socket-activated. It reloads the current bundle resolver for each accepted request so a running broker does not dispatch stale runner intents after a switch, and it never holds in-flight session state across requests. @@ -125,7 +125,7 @@ and the exact remediation command (`d2b vm restart ` for clean down+up, New per-VM work belongs **inside the daemon's DAG executor** (`packages/d2bd-runtime/src/supervisor/`), with privileged effects routed through a -typed `d2b-priv-broker` op declared in `packages/d2b-contracts/` and audited in +typed `d2b-broker` op declared in `packages/d2b-contracts/` and audited in `/var/lib/d2b/audit/broker-.jsonl`. Do not introduce a `systemd.services.*` declaration in `nixos-modules/` for per-VM work. Denylist coverage is owner-local or structural; run the focused Nix-unit and daemon tests when changing this surface. See diff --git a/docs/contributing/critical-subsystems.md b/docs/contributing/critical-subsystems.md index f6fd679bad..d7083c2d1c 100644 --- a/docs/contributing/critical-subsystems.md +++ b/docs/contributing/critical-subsystems.md @@ -35,9 +35,9 @@ daemon owns sync; there is no per-VM `store-sync` unit. ## TPM persistence (per-VM swtpm) -**Where:** `/var/lib/d2b/vms//swtpm/`; spawned via broker `SpawnRunner` from `packages/d2b-provider-device-tpm/src/swtpm_argv.rs` and supervised by `d2bd` as a child of the VM's DAG. The broker **provisions + hardens** this dir on first start (`packages/d2b-priv-broker/src/ops/swtpm_dir.rs`, gated on `seccomp_policy_ref == "w1-swtpm"`): fd-safe create (owner `d2b--swtpm`, mode 0700, inherited ACLs cleared), reconcile-in-place on a correct-owner existing dir, fail-closed on owner/type/symlink mismatch, ancestor `--x` traverse ACL, stale `tpm.sock` unlink - emitting the path-free `PrepareSwtpmDir` audit op. +**Where:** `/var/lib/d2b/vms//swtpm/`; spawned via broker `SpawnRunner` from `packages/d2b-provider-device-tpm/src/swtpm_argv.rs` and supervised by `d2bd` as a child of the VM's DAG. The broker **provisions + hardens** this dir on first start (`packages/d2b-broker/src/ops/swtpm_dir.rs`, gated on `seccomp_policy_ref == "w1-swtpm"`): fd-safe create (owner `d2b--swtpm`, mode 0700, inherited ACLs cleared), reconcile-in-place on a correct-owner existing dir, fail-closed on owner/type/symlink mismatch, ancestor `--x` traverse ACL, stale `tpm.sock` unlink - emitting the path-free `PrepareSwtpmDir` audit op. -Holds the per-VM TPM 2.0 NVRAM + EK seed. **Wiping it looks like device tampering to any IdP** (Entra ID, Intune, Bitlocker-style policies) and forces re-enrollment. Never zero it casually. The per-VM state root is `3770` (setgid **+ sticky**) so a non-owner role UID cannot rename/replace the `swtpm/` entry; an identity-bound, root-owned marker at `/var/lib/d2b/swtpm-markers/` makes a *previously-provisioned-then-missing/replaced* dir **fail the VM start closed** (`previously-provisioned-swtpm-state-missing`) rather than silently re-creating an empty TPM. The state directory's ACLs are asserted by `tests/unit/smoke/smoke-eval-tpm.nix`; the broker hardening by `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` tests. +Holds the per-VM TPM 2.0 NVRAM + EK seed. **Wiping it looks like device tampering to any IdP** (Entra ID, Intune, Bitlocker-style policies) and forces re-enrollment. Never zero it casually. The per-VM state root is `3770` (setgid **+ sticky**) so a non-owner role UID cannot rename/replace the `swtpm/` entry; an identity-bound, root-owned marker at `/var/lib/d2b/swtpm-markers/` makes a *previously-provisioned-then-missing/replaced* dir **fail the VM start closed** (`previously-provisioned-swtpm-state-missing`) rather than silently re-creating an empty TPM. The state directory's ACLs are asserted by `tests/unit/smoke/smoke-eval-tpm.nix`; the broker hardening by `packages/d2b-broker/src/ops/swtpm_dir.rs` tests. ## USBIP passthrough @@ -139,6 +139,38 @@ inventory, snapshot, or nested fixture workspace is required. Controller and core-reconciliation engines are test-only and unwired from the absent production store/watch dispatcher. An EffectPort call is permitted only after durable resource commit and consumption of the matching `CommittedRevisionProof`; abort, conflict, stale proof, or restart ambiguity cannot release an effect. Preserve per-resource single flight, bounded fair admission, deterministic owner/dependency propagation, and restart-safe idempotency when wiring the production path. +## Controller assignment and scoped routing + +**Where:** `packages/d2b-contracts-provider/src/v3/provider.rs`, `packages/d2b-core-controller/src/controller_assignment.rs`, `packages/d2b-bus/src/{authorization,router,session_seam_tests}.rs`, and `packages/d2bd/tests/resource_operator_activation.rs` + +Provider manifests carry a closed controller placement contract: +`instanceScope`, supported target kinds, per-target component artifacts, and +required EffectPort classes. Core owns one assignment registry. Each lease +binds the resource UID and revision, provider and controller generations, +controller role, exact target, session generation, and assignment epoch. +Target readiness, role scope, and resource placement are checked without a +fallback target. + +Resource-client leases are non-clonable and cannot widen their query or +mutation scope. Queries always add the assignment-owned resource filter and +reject caller-supplied attempts to replace it; mutating verbs carry the same +assignment identity. Scoped commit transport is versioned, size-bounded, and +rejects stale, forged, released, or revoked-session evidence. Host and Guest +controller routes therefore remain target-scoped and single-owner across +reconnects and handoff. + +## Broker host and guest profiles + +**Where:** `packages/d2b-broker/src/{runtime,protocol}.rs`, `packages/d2b-broker/tests/{guest_profile,host_profile,profile_separation}.rs`, `nixos-modules/{host-broker,guest-broker}.nix`, and `tests/unit/nix/surfaces/broker-profiles.nix` + +The broker profile is selected only at process start. Host and Guest +instances have distinct authority, socket, state, and audit bindings; a +request cannot select or switch profiles. The Guest profile admits only its +closed local operation set and rejects host-only effects before bundle +dispatch or mutation, while the Host profile retains the complete host +operation catalog. Keep these checks in the owner-local broker tests and +broker profile Nix cases; do not restore the retired central policy package. + ## Unsafe-local provider, launcher, and persistent-shell helper **Where:** `nixos-modules/options-realms-workloads.nix`, `nixos-modules/unsafe-local-workloads-json.nix`, `packages/d2b-core/src/unsafe_local_workloads.rs`, `packages/d2b-contracts-control/src/unsafe_local_wire.rs`, `packages/d2b-unsafe-local-helper/src/{shell_runtime,shell_supervisor,shell_socket,output_ring,tty_exec}.rs`, and `docs/reference/unsafe-local-provider.md` @@ -157,11 +189,11 @@ Version-pinned via `manifestVersion`. Adding, removing, or renaming a per-VM fie Sensitive bundle artifacts install at `root:d2bd` 0640 and ground every broker/sandbox/runner behaviour. `d2b-core` DTOs are canonical; `d2b._bundle` is the typed internal artifact table that owns JSON data, install names, classifications, and `/etc/d2b` materialization for every bundle artifact. Add new bundle artifacts through `nixos-modules/bundle-artifacts.nix` instead of hand-writing parallel install logic in each emitter. Committed schemas under `docs/reference/schemas/v2/` ARE the contract and `//packages/xtask:gen_schemas_drift` enforces their owner-local generator through `make test-drift`. Breaking the schema without an intentional `bundleVersion`/`schemaVersion` bump silently breaks every downstream consumer. -## Control plane - `d2bd` + `d2b-priv-broker` +## Control plane - `d2bd` + `d2b-broker` -**Where:** `packages/d2b-contracts/**` + `packages/d2b-core/**` + `packages/d2bd/**` + `packages/d2b-priv-broker/**` (sibling workspace; `unsafe_code = "deny"` with quarantined `src/sys.rs` for fd-passing FFI) + `packages/d2b/**` + `docs/reference/{cli-contract,daemon-api,error-codes,privileges}.md` + the fixed daemon Layer-1 gate set +**Where:** `packages/d2b-contracts/**` + `packages/d2b-core/**` + `packages/d2bd/**` + `packages/d2b-broker/**` (sibling workspace; `unsafe_code = "deny"` with quarantined `src/sys.rs` for fd-passing FFI) + `packages/d2b/**` + `docs/reference/{cli-contract,daemon-api,error-codes,privileges}.md` + the fixed daemon Layer-1 gate set -The **only** persistent root surfaces the framework declares. `d2b-priv-broker.socket` is socket-activated: systemd creates/binds/listens/sets-ACL before the broker starts; the broker adopts fd 3 via `SD_LISTEN_FDS` and MUST NOT self-bind, self-fchmod, or self-fchown when `SD_LISTEN_FDS=1`. `d2bd.service` carries `Wants=d2b-priv-broker.socket` (not `Requires=`) so the daemon keeps serving while the broker is idle. The broker reloads the current bundle resolver per accepted request so it does not dispatch stale runner intents after a switch. The broker drops to the `d2bd` group and uses `SO_PEERCRED` at accept time for authz (launcher / admin / deny). Every host mutation flows through a typed broker op (cgroup v2 delegation, TAP/bridge lifecycle, `ApplyNftables`, `ApplyNmUnmanaged`, `ApplySysctl`, `UpdateHostsFile`, `ModprobeIfAllowed`, `UsbipBindFirewallRule`, `SpawnRunner`, `OpenPidfd`) and is recorded as an `OpAuditRecord` in `/var/lib/d2b/audit/broker-.jsonl` (root-owned `0640 root:d2bd`, append-only `O_APPEND`, daily rotation, 14-day default retention overridable via `d2b.site.audit.retentionDays`). Relevant enforcing coverage includes `tests/unit/nix/cases/broker-socket-activation.nix`, `tests/unit/nix/cases/broker-caps.nix`, and daemon startup integration tests under `packages/d2bd/tests/`. Security-critical behavior remains owner-local or structural. See [ADR 0015](../adr/0015-daemon-only-clean-break.md). +The **only** persistent root surfaces the framework declares. `d2b-broker.socket` is socket-activated: systemd creates/binds/listens/sets-ACL before the broker starts; the broker adopts fd 3 via `SD_LISTEN_FDS` and MUST NOT self-bind, self-fchmod, or self-fchown when `SD_LISTEN_FDS=1`. `d2bd.service` carries `Wants=d2b-broker.socket` (not `Requires=`) so the daemon keeps serving while the broker is idle. The broker reloads the current bundle resolver per accepted request so it does not dispatch stale runner intents after a switch. The broker drops to the `d2bd` group and uses `SO_PEERCRED` at accept time for authz (launcher / admin / deny). Every host mutation flows through a typed broker op (cgroup v2 delegation, TAP/bridge lifecycle, `ApplyNftables`, `ApplyNmUnmanaged`, `ApplySysctl`, `UpdateHostsFile`, `ModprobeIfAllowed`, `UsbipBindFirewallRule`, `SpawnRunner`, `OpenPidfd`) and is recorded as an `OpAuditRecord` in `/var/lib/d2b/audit/broker-.jsonl` (root-owned `0640 root:d2bd`, append-only `O_APPEND`, daily rotation, 14-day default retention overridable via `d2b.site.audit.retentionDays`). Relevant enforcing coverage includes `tests/unit/nix/cases/broker-socket-activation.nix`, `tests/unit/nix/cases/broker-caps.nix`, and daemon startup integration tests under `packages/d2bd/tests/`. Security-critical behavior remains owner-local or structural. See [ADR 0015](../adr/0015-daemon-only-clean-break.md). ## Storage lifecycle / restart / synchronization @@ -175,11 +207,31 @@ Managed paths, restart adoption, locks, leases, cleanup, and degraded-state repo These are the framework's contract with consumers. Loosening one silently turns a previously-rejected misconfig into runtime breakage. New assertions need a matching case in `tests/unit/nix/cases/assertions.nix`. -## Guest-control exec session table - -**Where:** `packages/d2bd/src/{exec_session,exec_session_real}.rs`, `run_exec_owner` in `packages/d2bd/src/lib.rs`, `packages/d2b/src/exec_client.rs`, `packages/d2b-contracts-control/src/public_wire.rs` (`ExecOp`/`ExecOpResponse`) - -Arbitrary `d2b vm exec` is **admin-only**; configured `d2b launch` local-VM items may use the same detached guest-control backend with launcher authority because argv is resolved exclusively from the hash-verified private bundle. Both run through `d2bd` plus authenticated guest-control vsock to `guestd`. Attached exec uses the daemon's in-process **session table**: per-session workers own one authenticated guest-control client and proxy typed exec ops. **guestd runs every exec as the VM's workload user (`ssh.user`) inside a real PAM login session (`systemd-run --property=PAMName=login --uid=`) - never as root; the wire `user` field is ignored and the target user is host-fixed, bare `argv[0]` is resolved by the workload user's login `PATH`, and each attached exec runs in a process-unique named transient unit (`d2b-exec-<…>.service`) that teardown stops via `systemctl kill` so a quiet command cannot outlive owner-disconnect, cancel, or the runtime ceiling. Operators elevate with `sudo` inside the session.** Detached non-TTY exec is enabled with `d2b vm exec -d -- ` and managed through VM-first verbs (`d2b vm exec list`, `logs `, `status `, `kill `); command forms always require `--`, so those verb words remain valid VM names. Detached jobs and configured local-VM launches also run as the workload user, never root: the root detached runner only owns trusted slot/log files, re-validates the non-root uid before spawning the workload unit, and fails terminally rather than falling back to direct root execution. Guestd reconciles detached runner/workload units on startup, cleans orphaned workloads, and runs a periodic reaper for terminal records and retained logs; `kill` maps to idempotent two-phase `ExecCancel` (SIGTERM/grace/SIGKILL). There is **no per-VM systemd unit, no new broker op, and no SSH** - the guest owns the PTY; the host only flips termios for attached TTY via an RAII raw-mode guard restored on every exit/error/panic. The admin `SO_PEERCRED` check runs before arbitrary exec session setup; configured launch instead requires local launcher/admin authority and a trusted configured item. Old/non-guest-control generations fail closed (exit `70`) with no proxy and no SSH fallback. Session-table caps (global/per-UID/per-VM), detached slot/log quotas, and rate limits are enforced before connect/auth or create. Attached audit emits one redacted kind=critical session-establishment event (vm/peer_uid/tty); detached create/kill daemon audit carries only vm/peer_uid/action/result/exec_id, while configured-launch audit adds target/item/operation correlation without execution details. Opaque session handles, argv, stdio, env, cwd, and paths never reach any Debug/trace/audit/metric surface. Validate with the `exec_session`/`exec_client` hermetic test matrices. +## ComponentSession exec session table + +**Where:** `packages/d2bd-runtime/src/{exec_session,exec_session_real}.rs`, +`packages/d2b/src/exec_client.rs`, and +`packages/d2b-contracts-control/src/public_wire.rs` (`ExecOp`/`ExecOpResponse`) + +Arbitrary `d2b vm exec` is **admin-only**; configured `d2b launch` local-VM +items may use the same target-local Process backend with launcher authority +because argv is resolved exclusively from the hash-verified private bundle. +Both paths run through `d2bd` and an authenticated ComponentSession. Attached +exec uses the daemon's in-process **session table**, while target-local +Processes own command execution and the guest PTY. Detached non-TTY exec is +represented by `EphemeralProcess` resources and managed through bounded +Resource API operations. There is **no per-VM systemd unit, no direct feature +spawn, no broker operation for exec, and no SSH fallback**. The admin +`SO_PEERCRED` check runs before arbitrary exec session setup; configured launch +instead requires local launcher/admin authority and a trusted configured item. +Old or incompatible peers fail closed before controller authority or feature +behavior. Session-table caps, detached slot/log quotas, and rate limits are +enforced before connect/auth or create. Attached audit emits redacted +session-establishment events, while detached create/kill audit carries only +bounded target, peer, action/result, and opaque execution correlation. Opaque +session handles, argv, stdio, env, cwd, and paths never reach any +Debug/trace/audit/metric surface. Validate with the `exec_session` and +`exec_client` hermetic test matrices. ## Unsafe-local persistent shells @@ -201,9 +253,9 @@ The framework owns `${cfg.site.keysDir}/_ed25519`. `d2b keys rotate` MUST NO ## virtiofsd sandbox model -**Where:** `nixos-modules/minijail-profiles.nix` (virtiofsdProfiles), `packages/d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner` user-NS path), `nixos-modules/processes-json.nix` (argv emit) +**Where:** `nixos-modules/minijail-profiles.nix` (virtiofsdProfiles), `packages/d2b-broker/src/sys.rs` (`clone3_spawn_runner` user-NS path), `nixos-modules/processes-json.nix` (argv emit) -virtiofsd profiles MUST declare zero host capabilities (`capabilities = []`), `requiresStartRoot = false`, and a `userNamespace` block mapping in-NS UID/GID 0 to the per-share principal. Normal VM shares map to `d2b--runner`; the guest-control token share (`d2b-gctl`) maps to the narrower `d2b--gctlfs` principal. The broker pre-establishes the user namespace via `clone3(CLONE_NEWUSER)` + `pipe2` sync + `/proc//uid_map` writes BEFORE virtiofsd's first instruction runs. virtiofsd argv MUST include `--sandbox=chroot --inode-file-handles=never` and `--readonly` for every `readOnly` share (`ro-store`, `d2b-gctl`). Reintroducing host caps, `requiresStartRoot=true`, or `--sandbox=namespace` violates [ADR 0021](../adr/0021-broker-user-namespace-for-virtiofsd.md). Rendered profile and argv coverage is owned by the provider and broker tests. +virtiofsd profiles MUST declare zero host capabilities (`capabilities = []`), `requiresStartRoot = false`, and a `userNamespace` block mapping in-NS UID/GID 0 to the per-share principal. Normal VM shares map to `d2b--runner`; ComponentSession enrollment keys are not delivered through a virtiofs share. The broker pre-establishes the user namespace via `clone3(CLONE_NEWUSER)` + `pipe2` sync + `/proc//uid_map` writes BEFORE virtiofsd's first instruction runs. virtiofsd argv MUST include `--sandbox=chroot --inode-file-handles=never` and `--readonly` for every `readOnly` share (`ro-store`, `d2b-meta`, and host-key shares). Reintroducing host caps, `requiresStartRoot=true`, or `--sandbox=namespace` violates [ADR 0021](../adr/0021-broker-user-namespace-for-virtiofsd.md). Rendered profile and argv coverage is owned by the provider and broker tests. ## cgroup slice naming and ownership markers diff --git a/docs/explanation/daemon-experimental.md b/docs/explanation/daemon-experimental.md index 6938efb12e..c6c36b2241 100644 --- a/docs/explanation/daemon-experimental.md +++ b/docs/explanation/daemon-experimental.md @@ -3,7 +3,7 @@ ## v1.0 (ADR 0015): on by default In v1.0 the daemon-only end-state landed: every mutating verb dispatches -through `d2bd` → `d2b-priv-broker`, and the historical bash CLI +through `d2bd` → `d2b-broker`, and the historical bash CLI (`nixos-modules/cli.nix`) and the three-mode bridge (`default daemon-first` / `D2B_NATIVE_ONLY=1` / `D2B_LEGACY_BASH_OPT_IN=1`) were retired in v1.0. @@ -18,7 +18,7 @@ Enabling it adds the v1.0 daemon surface to the host: - the `d2bd` system user/group; - the `d2bd.service` unit plus the public/private sockets; -- the `d2b-priv-broker.{service,socket}` units (socket-activated; +- the `d2b-broker.{service,socket}` units (socket-activated; see ADR 0015); - the Rust CLI + manpages/completions; - `/etc/d2b/{bundle,host,processes,privileges}.json` emitted at diff --git a/docs/explanation/daemon-lifecycle.md b/docs/explanation/daemon-lifecycle.md index b0ff70c168..e02d45c8a1 100644 --- a/docs/explanation/daemon-lifecycle.md +++ b/docs/explanation/daemon-lifecycle.md @@ -13,14 +13,13 @@ control plane. Each VM declared in the public manifest gets its own [`VmProcessDag`](../reference/manifest-schema.md) under -`processes.json`. The headless shape is a linear 5-node DAG: +`processes.json`. The headless shape is a linear 4-node DAG: ```text host-reconcile └─→ store-preflight └─→ virtiofsd-ro-store - └─→ ch - └─→ guest-control-health + └─→ ch ``` Roles, from @@ -38,15 +37,10 @@ Roles, from argv emitted by the [`runtime-cloud-hypervisor`](../../packages/d2b-provider-runtime-cloud-hypervisor/src/vmm_argv.rs) Provider. -- `guest-control-health` - daemon-side authenticated guest-control - Health probe (full Hello + token challenge-response + Health over the - guest-control vsock). It is the framework readiness gate on - guest-control-capable VMs (`d2b.vms..guest.control.enable = - true`) and fails **closed**: never ready for an old-generation, - unreachable, auth-failed, or timed-out guest. Per-VM sshd/host-keys - are retained as a compat surface but never gate readiness: the - legacy raw TCP-22 `ssh-ready` / `guest-ssh-readiness` DAG node was - removed and is no longer emitted for any VM. +- `component-session` - the Guest-side `d2bd guest` target agent owns the + enrolled parent-Zone ComponentSession. It is not a host DAG node and does + not create a host public socket or local Zone store; session evidence and + target-local resource status provide readiness. Optional roles wired by per-VM features: @@ -101,12 +95,10 @@ Supported predicate kinds (per - `tcp-port: { host, port }` - TCP `connect()` against `host:port`. A generic predicate kind retained for old-generation compatibility; the framework no longer emits it as the readiness - signal (see `guest-control-health` below). -- `guest-control-health: { vm }` - daemon-side authenticated - guest-control Health probe. Fails **closed**: ready only when the - daemon completes the authenticated Hello + token challenge-response + - Health exchange over the guest-control vsock. This is the framework - readiness gate for guest-control-capable VMs. + signal (see `component-session-health` below). +- `component-session-health: { vm }` - legacy daemon-side component-session Health + probe retained only for old bundle decoding; new Nix output does not emit + this predicate. - `command: [argv...]` - daemon-spawned probe child exits 0. - `component-specific: ` - escape hatch named by the role's emitter; the supervisor delegates the check. @@ -155,8 +147,8 @@ primary VMM stop/cleanup decision. ## Host shutdown and reboot integration NixOS still declares only the three ADR-0015 root-visible units: -`d2bd.service`, `d2b-priv-broker.socket`, and -`d2b-priv-broker.service`. There is no per-VM or extra guest-shutdown +`d2bd.service`, `d2b-broker.socket`, and +`d2b-broker.service`. There is no per-VM or extra guest-shutdown systemd unit. Instead, `d2bd.service` has an `ExecStop` hook that first checks the systemd manager state with absolute systemd helper paths. It runs the all-VM shutdown hook only when the system manager is stopping for host diff --git a/docs/explanation/design.md b/docs/explanation/design.md index 8c990dd355..f8c0e7a388 100644 --- a/docs/explanation/design.md +++ b/docs/explanation/design.md @@ -320,7 +320,7 @@ Pretending otherwise would be dishonest. Every mutating verb - lifecycle (`vm start`/`vm stop`/`vm restart`/ `switch`), host-prepare, key rotation, USBIP bind, store verify, config sync (`readGuestConfig`), and the destructive - guest-control exec verb (`vm exec`, which runs commands as the VM's + component-session exec verb (`vm exec`, which runs commands as the VM's workload user in a PAM login session - never as root) - is gated to the admin role (`d2b.site.adminUsers`, checked via `SO_PEERCRED` at accept @@ -475,7 +475,7 @@ Everything else is derived. > `nixos-modules/host-wrapper.nix` were deleted in v1.0. The section > below documents the legacy architecture for historical context; > in v1.0 the per-VM lifecycle is fully owned by `d2bd`'s -> supervisor DAG dispatched through `d2b-priv-broker`'s +> supervisor DAG dispatched through `d2b-broker`'s > `SpawnRunner` / `SignalRunner` ops, and runner lifecycle-of-record > is the broker-registered pidfd table. See > [§ Launcher authorisation](#launcher-authorisation-v10-so_peercred--d2b-group) @@ -527,7 +527,7 @@ audit and backup scripts can reason about. ### Per-VM sidecars > **v1.0 status (ADR 0015):** in v1.0 daemon-only, the per-VM sidecars -> listed below are spawned by `d2b-priv-broker` via the supervisor +> listed below are spawned by `d2b-broker` via the supervisor > DAG (`SpawnRunner` requests) and registered in the pidfd table for > lifecycle ownership. Historically, they were emitted as > independent systemd units under `nixos-modules/host-sidecars.nix`; in @@ -605,10 +605,10 @@ up sidecar config changes. The framework provides two paths: closure. Use this when `d2b guest list` exposes a pending update in `[pending restart]` after a `nixos-rebuild switch`. - `d2b activation switch Guest/ --apply` - full Guest closure rebuild + live - activation through the daemon's authenticated guest-control path + activation through the daemon's authenticated component-session path (no VM reboot). Use this when you edited the VM's own NixOS module. The host publishes the prepared toplevel into the VM's store view; - guestd runs the activation inside the guest, not through host + target-local Process runs the activation inside the guest, not through host systemd. #### Pending-restart detection via `booted` vs `current` @@ -715,8 +715,8 @@ That same store-view boundary is the activation boundary. A host build produces the VM's NixOS `system.build.toplevel`, then the broker/store-view path publishes the closure into the per-VM live store pool. The guest sees the pool through virtiofs as its `/nix/store`. -For live `switch`, `test`, and `rollback`, `d2bd` asks guestd over -authenticated guest-control to activate the prepared toplevel inside +For live `switch`, `test`, and `rollback`, `d2bd` asks target-local Process over +authenticated component-session to activate the prepared toplevel inside the running VM and waits for guest activation status before committing host generation metadata through the broker. If the VM is stopped, offline, or too old to advertise guest activation, live activation @@ -737,8 +737,8 @@ is the daily-driver interface. Verbs include `list`, `status`, `keys`, `host`, and `audit`. The CLI is the Rust binary, full stop: the pre-v1.0 bash CLI (and the generated `nixos-modules/cli.nix` shell script) was retired in v1.0. Host mutations dispatch through -`d2bd` → `d2b-priv-broker`; live guest activation dispatches -through `d2bd` → authenticated guest-control and only uses the +`d2bd` → `d2b-broker`; live guest activation dispatches +through `d2bd` → authenticated component-session and only uses the broker to publish/commit host-owned generation metadata (see [ADR 0015](../adr/0015-daemon-only-clean-break.md) and [ADR 0017](../adr/0017-no-bash-fallbacks-invariant.md)). The @@ -841,7 +841,7 @@ patterns, see [the naming conventions reference](../reference/naming-conventions | VM name (`d2b.vms.`) | `^[a-z][a-z0-9-]*$`, ≤ ... no `sys-` prefix, not `launcher` | assertions.nix | | Env name (`d2b.envs.`) | `^[a-z][a-z0-9-]*$`, length ≤ 8 (IFNAMSIZ-1=15 minus `br--lan` = 7) | network.nix | | `d2bd.service` | non-root daemon (v1.0; per ADR 0015 the only persistent user-facing d2b unit besides the broker) | host-daemon.nix | -| `d2b-priv-broker.{socket,service}` | socket-activated privileged broker (v1.0) | host-broker.nix | +| `d2b-broker.{socket,service}` | socket-activated privileged broker (v1.0) | host-broker.nix | | `microvm@.service` | upstream microvm.nix unit (still emitted for direct-debug bypass; not the lifecycle-of-record in v1.0) | microvm.nix | | `d2b.slice//` | per-VM broker-spawned runner leaves (v1.0 replaces legacy `d2b--{gpu,video,snd,swtpm,store-sync,usbip}.service`) | broker SpawnRunner | | `d2b.slice/sys-/usbipd-{backend,proxy}` | per-env USBIP runner leaves (v1.0 replaces legacy `d2b-sys--usbipd-{backend,proxy}.service`) | broker SpawnRunner | @@ -949,12 +949,12 @@ uid/gid, refuses requests from peers outside the `d2b` group / mutating verbs to the admin role (`d2b.site.adminUsers`, the *role* gate). Per-verb authorisation lives in the daemon's `dispatch_request` table. Most host-mutating verbs route through -`d2b-priv-broker`, where each host mutation is recorded as an +`d2b-broker`, where each host mutation is recorded as an audited `OpAuditRecord` in `broker-.jsonl`. The -guest-control verbs are the exception: `readGuestConfig` -(config sync) reads the guest's config over the typed guest-control +component-session verbs are the exception: `readGuestConfig` +(config sync) reads the guest's config over the typed component-session channel rather than mutating the host, and `vm exec` -proxies a guest-control exec session - running as the VM's workload +proxies a component-session exec session - running as the VM's workload user (`ssh.user`, never root) in a PAM login session - whose establishment and termination are recorded as *leak-safe daemon-side* lifecycle events in @@ -1226,8 +1226,8 @@ evaluation, so it is contained on three independent axes (see tree. An approved file is trusted, operator-reviewed host Nix - no more privileged than config the operator writes by hand. - **No new attack surface.** The transport is a host-initiated read - over the authenticated guest-control vsock (the daemon's - `ReadGuestConfig` → guestd `ReadGuestFile` path) - no virtiofs share, + over the authenticated component-session vsock (the daemon's + `ReadGuestConfig` → target-local Process `ReadGuestFile` path) - no virtiofs share, no new socket, no writable host-backed mount; the guest never initiates a connection into the host control plane, and there is no SSH fallback (an old-generation guest that does not advertise @@ -1395,7 +1395,7 @@ signal: existing closure. Use this when you ran `nixos-rebuild switch` and a sidecar config changed. - `d2b activation switch Guest/ --apply` does a Guest closure rebuild + live - guest-control activation through the daemon (no VM reboot). Use this + component-session activation through the daemon (no VM reboot). Use this when you edited the VM's own NixOS module. Stopped VMs use `d2b boot --apply` for offline staging instead of host-side activation. @@ -1606,7 +1606,7 @@ portability work splits that into three layers: `SO_PEERCRED` at `accept(2)` time. The daemon then resolves the peer uid against `d2b.site.launcherUsers` (connection) and `d2b.site.adminUsers` (the *role* gate for destructive / - host-prepare / key-rotation / guest-control verbs). There is no + host-prepare / key-rotation / component-session verbs). There is no separate `d2b-admin` socket or group - admin is a `SO_PEERCRED`-derived role on the single public socket, not a second endpoint. See ADR 0002. diff --git a/docs/explanation/host-prepare.md b/docs/explanation/host-prepare.md index 7d96973a58..3ff25a4aec 100644 --- a/docs/explanation/host-prepare.md +++ b/docs/explanation/host-prepare.md @@ -40,7 +40,7 @@ prepare` can answer four questions before touching the command: `d2bd` runs as an unprivileged system user. It performs no privileged mutation itself; instead, every mutating step is forwarded -to `d2b-priv-broker` over a `0600` private Unix socket +to `d2b-broker` over a `0600` private Unix socket (`priv.sock`). The broker: - only ever speaks a **closed enum** of typed operations (see diff --git a/docs/explanation/persistent-shells.md b/docs/explanation/persistent-shells.md index 53050df7f8..172e041b02 100644 --- a/docs/explanation/persistent-shells.md +++ b/docs/explanation/persistent-shells.md @@ -25,7 +25,7 @@ expected to survive: - the local CLI disconnecting; - the terminal window closing; -- guestd restart when guestd can adopt the still-running shell pool; +- target-local Process restart when target-local Process can adopt the still-running shell pool; - unsafe-local helper or d2bd reconnect while the verified user scope and supervisor remain alive. @@ -50,7 +50,7 @@ not load realm credentials or provider transports. Persistent shells do not add TCP or UDP listeners, network ports, or network-bound debug/metrics surfaces. The host-to-guest path reuses the existing -daemon public socket and authenticated guest-control transport. +daemon public socket and authenticated component-session transport. Unsafe-local uses only same-UID Unix sockets. Its per-shell listener lives beneath the validated user runtime directory and is not a root service, broker diff --git a/docs/explanation/storage-lifecycle.md b/docs/explanation/storage-lifecycle.md index 1cc2a92859..79654742fb 100644 --- a/docs/explanation/storage-lifecycle.md +++ b/docs/explanation/storage-lifecycle.md @@ -127,13 +127,13 @@ attacker cannot flood the violation lane and evict normal audit history. ## Migration posture -The storage cutover is allowed to be disruptive once. During that planned +The storage layout migration is allowed to be disruptive once. During that planned downtime, d2b may clear old boot-scoped runtime files and lock records after proving VMs and runner processes are stopped. Persistent data such as TPM NVRAM, store-view metadata, SSH keys, daemon adoption metadata, audit history, host-runtime metadata, and disk images is preserved and verified. -After the cutover, normal restarts are continuation events again. +After the migration, normal restarts are continuation events again. ## Logical backup and restore diff --git a/docs/explanation/unsafe-local-runtime.md b/docs/explanation/unsafe-local-runtime.md index 4de7577b7f..19b32a0f04 100644 --- a/docs/explanation/unsafe-local-runtime.md +++ b/docs/explanation/unsafe-local-runtime.md @@ -61,7 +61,8 @@ Firefox-specific, browser-specific, or URL-specific fields. A shell item owns presentation metadata and selects persistent-shell semantics. This keeps provider routing common: -- local VM exec items use authenticated guest control; +- local VM exec items use authenticated ComponentSession and target-local + Process resources; - unsafe-local exec items use the same-uid helper; - provider-managed runtimes advertise or refuse configured launch; - shell items use the persistent-shell operation family. diff --git a/docs/explanation/usb-security-key-architecture.md b/docs/explanation/usb-security-key-architecture.md index 0a2a15d6ed..ab1144722d 100644 --- a/docs/explanation/usb-security-key-architecture.md +++ b/docs/explanation/usb-security-key-architecture.md @@ -103,7 +103,7 @@ d2bd (host daemon) - lease serializer │ queues, timeouts, cancellation, audit log ▼ broker op UsbSecurityKeyOpenDevice → SCM_RIGHTS fd - │ d2b-priv-broker resolves stable selector → opens /dev/hidrawN + │ d2b-broker resolves stable selector → opens /dev/hidrawN ▼ physical YubiKey (host hidraw node) │ CTAP HID reports, user touch, responses @@ -150,7 +150,7 @@ physical key: ### Privileged device access Opening `/dev/hidrawN` for a FIDO-class device requires elevated privilege. The -broker dispatches a `UsbSecurityKeyOpenDevice` operation to `d2b-priv-broker`, +broker dispatches a `UsbSecurityKeyOpenDevice` operation to `d2b-broker`, which: 1. Resolves the configured stable selector to a `/dev/hidrawN` path. @@ -159,7 +159,7 @@ which: 3. Opens the path and returns the file descriptor to `d2bd` via `SCM_RIGHTS`. 4. Logs the operation to the d2b audit log. -`d2b-priv-broker` does not hold the file descriptor after the transfer; it is +`d2b-broker` does not hold the file descriptor after the transfer; it is owned exclusively by `d2bd` for the lifetime of the active ceremony. --- diff --git a/docs/how-to/configure-desktop-terminal-integration.md b/docs/how-to/configure-desktop-terminal-integration.md index 9d887df991..6e985f49f3 100644 --- a/docs/how-to/configure-desktop-terminal-integration.md +++ b/docs/how-to/configure-desktop-terminal-integration.md @@ -26,10 +26,7 @@ realm workload row that points at it: ```nix d2b.vms.work = { - ssh.user = "alice"; - - guest.control.enable = true; - guest.exec.enable = true; + guest.componentSession.enable = true; guest.shell.enable = true; }; @@ -115,7 +112,7 @@ this rendered shape. When realm workload metadata is present, d2b-wlterm groups shell-capable workloads by realm and displays canonical targets such as -`shellbox.work.d2b`. It still uses the d2bd public socket and guest-control +`shellbox.work.d2b`. It still uses the d2bd public socket and component-session capability checks; it does not read root-owned d2b bundle artifacts directly. ## Configure Waybar @@ -152,5 +149,5 @@ d2b guest status shellbox ``` If `d2b-wlterm list` reports a typed shell capability error, confirm that the -VM has `guest.control.enable`, `guest.exec.enable`, and `guest.shell.enable` -set and that the VM was restarted after switching the host configuration. +VM has `guest.componentSession.enable` and `guest.shell.enable` set and that +the VM was restarted after switching the host configuration. diff --git a/docs/how-to/cut-over-to-d2b-v3.md b/docs/how-to/cut-over-to-d2b-v3.md deleted file mode 100644 index 66a8632f50..0000000000 --- a/docs/how-to/cut-over-to-d2b-v3.md +++ /dev/null @@ -1,221 +0,0 @@ -# Cut over a daily-driver host to d2b v3 - -This runbook describes the one-time, host-wide cutover. It is deliberately -separate from scoped Zone, Provider, or Guest reset. The cutover lane stops at -`CutoverSucceeded`; merge, post-merge seal, and irreversible finalization -belong to U7. - -## Before binding - -Use one clean, committed candidate and one prebuilt closure. Freeze and record: - -- candidate id, commit, tree, bundle generation, closure path, and GC root; -- the canonical, host-wide preview digest; -- the exact operator subject and host identity digests; -- the typed apply consent and its digest; -- the typed host-generation handoff; -- the strict external recovery-point attestation. - -The recovery point must cover the complete host, every configured Zone in the -preview, boot and system state, preserved identity state, and the exact -restore instructions. It must be read-only through expiry and must have enough -remaining lifetime for apply, verification, guarded merge, and post-merge -seal. A local checkpoint or a copy under `/var/lib/d2b` is not the R19 -recovery point. - -Validate the candidate through the production delivery stages before any host -mutation: - -```bash -bazel build //packages/xtask:xtask -bazel-bin/packages/xtask/xtask delivery wave recovery-import \ - --snapshot \ - --attestation \ - --repo = \ - --candidate-id \ - --commit-oid \ - --tree-oid \ - --closure-store-path-sha256 \ - --bundle-generation \ - --preview-sha256 \ - --host-identity-sha256 \ - --operator-subject-sha256 \ - --restore-instructions-sha256 \ - --recovery-point-locator-sha256 \ - --required-remaining-ttl-seconds \ - --verifier-now-unix \ - --command \ - --state-dir - -bazel-bin/packages/xtask/xtask delivery wave seal \ - --snapshot \ - --repo = \ - --state-dir - -bazel-bin/packages/xtask/xtask delivery wave merge-eligibility \ - --seal \ - --repo = \ - --state-dir -``` - -The validators own the allow/refuse predicates. Do not reproduce them in a -shell wrapper. - -## Preview and consent - -Run the preview without `--zone`. A one-time cutover is always host-wide: - -```bash -d2b host cutover preview \ - --operation-id \ - --candidate-id \ - --revision-plan-id \ - --system-artifact-id \ - --source-system-artifact-id \ - --json -``` - -Confirm `state=planned`, `phase=0`, `mutationAccepted=false`, -`inventory.complete=true`, and that `inventory.zoneCount` equals the complete -configured Zone inventory. Repeat the preview and require the same digest. -Any missing, unexpected, duplicate, or incomplete Zone is a no-go. - -The apply consent must be the canonical record bound to the exact operation, -candidate, preview, recovery digest, candidate artifact, preserved source -artifact, and operator. The apply handoff and preview must carry the same -candidate artifact identity. A rollback handoff must carry the admitted -preserved source artifact identity. A changed byte, stale digest, replayed -record, different operator, or different artifact is refused before bootstrap. - -## Apply and observe - -The production CLI admits the runner through `d2bd` and the broker before the -control plane is drained: - -```bash -d2b host cutover apply \ - --operation-id \ - --candidate-id \ - --revision-plan-id \ - --source-system-artifact-id \ - --preview-digest \ - --recovery-digest \ - --operator-id \ - --consent-digest \ - --consent-file \ - --recovery-attestation-file \ - --host-digest \ - --handoff-file \ - --json -``` - -The U3 recovery JSON is the compact operation-bound record consumed by the -cutover engine. The U5 attestation is the larger delivery record consumed by -`recovery-import`; both must bind the same candidate, preview, host, operator, -and restore instructions. - -Use the runner socket while the daemon is drained: - -```bash -d2b host cutover status --operation-id --json -d2b host cutover hold --operation-id --reason "" --json -d2b host cutover resume --operation-id --json -d2b host cutover doctor --operation-id --json -``` - -`status` is redaction-safe. `held` means no new effect starts; an in-flight -atomic effect may finish first. Any configured Admin may set a hold. Only the -bound operator may resume without fresh consent. `doctor` must not report -`cutover-quarantined`, `adoption-quarantined`, unresolved ownership, or -recovery degradation. - -The runner owns one durable journal and one host-wide OFD lock. A daemon or -client restart must leave the runner active and must not transfer repair -ownership to `d2bd`. - -## Rollback and external restore - -The rollback boundary is phase 4, after disposition staging and before the -resource store is committed: - -```bash -d2b host cutover rollback \ - --operation-id \ - --handoff-file \ - --json -``` - -Through phase 4, native rollback restores the preserved source generation, -keeps TPM, durable Volume, store-view, SSH-key, and audit bytes intact, and -quarantines staged destinations. It is not a filesystem-wide transaction. - -At phase 5 (`resource-store`) and later, native rollback is refused. A -failure, expiry, ambiguous destination, or candidate mismatch becomes -`RestoreRequired`. Stop issuing cutover commands and use only the qualified -external recovery mechanism. Do not attempt a replacement candidate, -finalization, or a second native rollback. Record the restore result and -re-run the same identity and audit digest checks after the host is booted. - -The VM rehearsal must exercise this phase-5 failure class with the same -recovery mechanism used by the operator. The daily-driver attestation binds -only digests of the locator and restore instructions; it must never contain a -raw URL, mount path, hostname, uid, operator name, credential, token, or -backup payload. - -## Verify and stop - -After all typed effects complete, provide observations for every configured -Zone: - -```bash -d2b host cutover verify \ - --operation-id \ - --verification-file \ - --json - -d2b host cutover doctor --operation-id --json -d2b host cutover status --operation-id --json -``` - -Verification must prove: - -- the observed Zone set exactly equals the configured Zone set; -- every Zone is healthy; -- preserved sources and identity digests match; -- the candidate is still current; and -- the broker audit publication is durable and continuous. - -The required stop state is `CutoverSucceeded`. Do not invoke -`d2b host cutover finalize` in this lane. - -## Finalization is separate - -Only after the guarded merge, post-merge reconciliation, post-merge seal, -fresh status/verify/doctor checks, and a new operator decision may U7 issue the -phase-10 consent: - -```bash -d2b host cutover finalize \ - --operation-id \ - --consent-file \ - --finalization-file \ - --json -``` - -The phase-10 consent is bound to a different finalization binding. Apply -consent cannot authorize it. Until that second consent is accepted, all -legacy sources remain preserved and available to the external restore path. - -## Redaction rules - -Keep operator logs limited to fixed labels, stable status classes, phase -numbers, and digest values intended for the candidate record. Suppress command -arguments and raw validator output from the live wrapper. Never print: - -- candidate state roots, store paths, sockets, hostnames, or process ids; -- recovery locators, restore text, backup names, or provider credentials; -- usernames, UIDs, SSH key material, tokens, or realm principals; or -- unredacted journal, audit, support-bundle, or terminal output. - -Use synthetic RFC1918/RFC5737 examples in rehearsal evidence and describe -redacted failures by their typed class rather than copying raw diagnostics. diff --git a/docs/how-to/edit-vm-config-from-inside.md b/docs/how-to/edit-vm-config-from-inside.md index 42533007e3..bb71257bf7 100644 --- a/docs/how-to/edit-vm-config-from-inside.md +++ b/docs/how-to/edit-vm-config-from-inside.md @@ -49,21 +49,21 @@ Rebuild the host once (`d2b activation switch Guest/work --apply`). The guest no - `/var/lib/d2b-guest/guest-config.nix` - a **writable** working copy, seeded once from the baseline. It is owned by the VM's `ssh.user` when one is declared, and by `root` otherwise (the - guest-control exec path can edit it either way). + component-session exec path can edit it either way). -### Prerequisite: the guest-control channel +### Prerequisite: the component-session channel `config sync` reads the edited file over the authenticated -**guest-control** vsock - the daemon's `ReadGuestConfig` → -guestd `ReadGuestFile` path - not over SSH. It is wired exactly when -the VM both enables guest-control and declares a `guestConfigFile`: +**component-session** vsock - the daemon's `ReadGuestConfig` → +target-local Process `ReadGuestFile` path - not over SSH. It is wired exactly when +the VM both enables component-session and declares a `guestConfigFile`: ```nix -d2b.vms.work.guest.control.enable = true; # the guest-control credential + guestd service +d2b.vms.work.guest.componentSession.enable = true; # enable the target-local Process service d2b.vms.work.guestConfigFile = ./vms/work.guest.nix; ``` -With those set, guestd advertises the `ReadGuestFile` capability and +With those set, target-local Process advertises the `ReadGuestFile` capability and serves a bounded read of exactly the working-copy path. Without them the capability stays absent and `config sync` **fails closed** with a typed error - it never falls back to SSH. `ssh.user` is **not** @@ -92,7 +92,7 @@ When `ssh.user` is unset the working copy is owned by `root`. d2b activation config sync Guest/work ``` - This pulls the edited file over the authenticated guest-control + This pulls the edited file over the authenticated component-session channel into a host-side staging copy (`~/.local/state/d2b/config-staging/work.guest.nix`). The host treats it as untrusted data - nothing is evaluated yet. @@ -144,7 +144,7 @@ edit isn't silently forgotten before you approve it. - The CLI never auto-writes your config tree: `approve` only writes the `--to` path you name. It never touches anything you don't point it at. - `config sync` is host-initiated (the host reads the guest's working - copy over the authenticated guest-control vsock). The guest never + copy over the authenticated component-session vsock). The guest never initiates a connection to the host control plane, and there is no new socket or virtiofs share. - If `/var` is not persistent in your VM, the writable working copy is diff --git a/docs/how-to/headless-alpha-walkthrough.md b/docs/how-to/headless-alpha-walkthrough.md index 9b98f24f23..0c0ea31269 100644 --- a/docs/how-to/headless-alpha-walkthrough.md +++ b/docs/how-to/headless-alpha-walkthrough.md @@ -109,13 +109,13 @@ is stable; `--apply` routes through the daemon-native dispatch {"id": "store-preflight", "role": "store-virtiofs-preflight"}, {"id": "virtiofsd-ro-store", "role": "virtiofsd"}, {"id": "ch", "role": "cloud-hypervisor-runner"}, - {"id": "guest-control-health", "role": "guest-control-health"} + {"id": "component-session-health", "role": "component-session-health"} ], "edges": [ {"from": "host-reconcile", "to": "store-preflight"}, {"from": "store-preflight", "to": "virtiofsd-ro-store"}, {"from": "virtiofsd-ro-store", "to": "ch"}, - {"from": "ch", "to": "guest-control-health"} + {"from": "ch", "to": "component-session-health"} ] }, "notes": "vm dry-run reports the DAG the supervisor would drive; --apply routes through the daemon-native dispatch (v1.0 daemon-only per ADR 0015)." @@ -141,12 +141,10 @@ is different from the original draft: `SpawnRunner` handler is live and returns pidfds over SCM_RIGHTS; the daemon can re-open / re-adopt them through the live `OpenPidfd` path. -4. `guest-control-health` - the daemon runs the authenticated - guest-control Health probe (Hello + token challenge-response + - Health over the guest-control vsock) on guest-control-capable VMs. - It fails closed and is the guest-readiness gate; SSH is a compat - surface only, so the legacy raw TCP-22 `ssh-ready` / - `guest-ssh-readiness` node was removed and is no longer emitted. +4. `component-session-health` - the daemon runs the authenticated + ComponentSession Health probe on ComponentSession-enabled VMs. It fails + closed and is the Guest-readiness gate; no raw TCP-22 readiness or SSH + execution fallback is emitted. The operator-facing routing changed: `d2b guest start corp-vm --apply` no longer stops at the old `daemon-down` placeholder by default. diff --git a/docs/how-to/migrate-d2b-v1-0-to-v1-1.md b/docs/how-to/migrate-d2b-v1-0-to-v1-1.md index 6876263462..456dcbf0b7 100644 --- a/docs/how-to/migrate-d2b-v1-0-to-v1-1.md +++ b/docs/how-to/migrate-d2b-v1-0-to-v1-1.md @@ -6,6 +6,10 @@ v1.0 (released 2025-Q4) and v1.1 (released 2026-Q2). v1.1 is the listed under CHANGELOG § "Deferred items" is closed, and several latent v0.x compatibility shims are removed. +> Historical migration guide. Current v3 Guests use `d2bd guest`, +> ComponentSession, and signed target-local Processes; this guide does not +> describe the current Guest control contract. + ## Prerequisites Before upgrading from v1.0 to v1.1, satisfy these prerequisites in diff --git a/docs/how-to/migrate-d2b-v1-2-to-v2.md b/docs/how-to/migrate-d2b-v1-2-to-v2.md index 63ac5247d4..d057a66fef 100644 --- a/docs/how-to/migrate-d2b-v1-2-to-v2.md +++ b/docs/how-to/migrate-d2b-v1-2-to-v2.md @@ -6,6 +6,10 @@ This guide covers the v2 realm-native control-plane transition. It is written for operators with an existing v1.2/v1.3 local d2b host and one or more local VMs already running on `d2b.envs` / `d2b.vms..env`. +> Historical migration guide. Current v3 Guests use `d2bd guest`, +> ComponentSession, and signed target-local Processes; the older transport +> references below are retained only for v1.x/v2 upgrades. + ## What changes - Existing local VM names and `d2b.vms..env` placement continue to work. @@ -44,7 +48,7 @@ large VM state, rewrite `/var/lib/d2b/vms/`, or migrate network bridges. ## Step 1: keep existing envs and VMs -Do not remove or rename `d2b.envs` or `d2b.vms..env` during the v2 cutover. +Do not remove or rename `d2b.envs` or `d2b.vms..env` during the v2 transition. Those declarations remain the active runtime substrate. For example, keep this: diff --git a/docs/how-to/migrating-from-microvm.md b/docs/how-to/migrating-from-microvm.md index 1cbf78f144..d25ba9ff38 100644 --- a/docs/how-to/migrating-from-microvm.md +++ b/docs/how-to/migrating-from-microvm.md @@ -518,7 +518,7 @@ d2b guest restart --apply ``` (Or `d2b activation switch Guest/ --apply` if you want a Guest closure rebuild + -live activation via guest control; restart cycles the existing closure +live activation through the target-local Process; restart cycles the existing closure cleanly.) `d2b guest status ` prints the Guest phase, conditions, and diff --git a/docs/how-to/troubleshoot-usbip.md b/docs/how-to/troubleshoot-usbip.md index 98427e92c7..6d464bd6d1 100644 --- a/docs/how-to/troubleshoot-usbip.md +++ b/docs/how-to/troubleshoot-usbip.md @@ -6,6 +6,12 @@ VM restart. For field-level contracts, see [`components-usbip.md`](../reference/components-usbip.md) and [`usb-probe.md`](../reference/cli-output/usb-probe.md). +> **Current U10 limitation:** the signed target-local USBIP Process and its +> authenticated ComponentSession effect adapter are not wired into `d2bd`. +> Non-qemu attach, detach, and VM-start USBIP reconciliation therefore fail +> closed before host mutation. Probe remains read-only and reports Guest import +> as unavailable; a host claim alone is not a successful attachment. + ## Before you start Run the commands as a d2b admin: either a user in the `d2b` lifecycle @@ -27,8 +33,9 @@ Host prerequisites: match an allowlist entry. Guest prerequisites are provided by `usbip.yubikey = true`: the guest loads -`vhci_hcd`, includes the guest `usbip` tool, and exposes guest-control USBIP -status/import operations. +`vhci_hcd` and includes the guest `usbip` tool. The target-local Process and +authenticated ComponentSession USBIP status/import operations remain a future +signed Process contract. Example host snippet: @@ -88,92 +95,58 @@ A healthy row is `bound` with the desired session claim and converged host, proxy, and guest state. A same-VM session claim after restart is expected to show as `degraded` until active carrier state is replayed. -## 3. Attach a declared device +## 3. Check attach availability -Use the busid from the declaration/probe. The VM must be running before apply -because guest-control performs the in-guest import. +Use the busid from the declaration/probe when validating the declaration. The +current non-qemu apply path is unavailable and does not bind the host device: ```bash -d2b guest start corp-vm --apply d2b device usb attach corp-vm 1-2 --apply -d2b device usb probe ``` -If the VM is stopped, attach fails before host mutation. Start the VM, wait for -it to report running, then retry the same attach command. +Expect a typed `runtime-capability-unsupported` error until the signed +target-local USBIP Process path is shipped. ## 4. Recover a same-VM session claim after restart VM stop/restart preserves same-VM USBIP session claims within the current host -boot/session but tears down active carriers/imports where safe. On the next -VM start, d2b replays host bind/proxy state and asks guestd to import again. -After a host reboot, `/run/d2b/locks/usbip` is recreated empty and the -operator should attach the device again. To verify the replay after an -intentional VM restart, run: - -```bash -d2b device usb probe -d2b guest restart corp-vm --apply -d2b device usb probe -``` - -If the post-restart probe still prints a degraded row, prefer its `command:` -line. For the common `guest-import-unavailable` or carrier replay case, run: +boot/session. Until Guest detach/import is available, d2b does not replay host +bind/proxy state and preserves the claim instead. After a host reboot, +`/run/d2b/locks/usbip` is recreated empty. Use probe to inspect the observed +state: ```bash -d2b device usb attach corp-vm 1-2 --apply d2b device usb probe ``` -If the row shows the same VM still owns the session claim and the host is already -bound (`SESSION-CLAIM=held-by-desired-owner`, -`HOST-BIND=bound-to-usbip-host`) but `GUEST=detached`, this is a convergable -same-owner state. Re-run the printed `d2b device usb attach --apply` -command. The daemon rechecks the per-env firewall/proxy path and asks guestd to -import the device again; it does not release the claim or require raw host -`usbip` commands. - -If the VM is stopped instead of restarted, start it first: - -```bash -d2b guest start corp-vm --apply -d2b device usb attach corp-vm 1-2 --apply -d2b device usb probe -``` - -Do not release a session claim just because the active carrier is down; release -it only when you want the VM to stop owning that busid during this host session. +Do not release a session claim just because the active carrier is down; the +current daemon cannot safely perform Guest detach or host release. ## 5. Release a claim -For a normal release: +The current non-qemu detach path is unavailable and preserves the claim: ```bash d2b device usb detach corp-vm 1-2 --apply d2b device usb probe ``` -If detach reports that revocation cannot be isolated, use the busid named in -the error. D2b could not prove that one busid stream can be revoked without -affecting unrelated same-env streams. Stop the owning VM so the stream drains, -then detach again: - -```bash -d2b guest stop corp-vm --apply -d2b device usb detach corp-vm 1-2 --apply -d2b device usb probe -``` - -Only use an explicit env-level drain/recycle operation when bouncing unrelated -same-env USB streams is acceptable. +Expect a typed `runtime-capability-unsupported` error. Do not edit the lock, +unbind the host device, or stop a shared proxy manually; release is deferred +until Guest detach can be authenticated and drained. ## Common troubleshooting +Until the U10 limitation is removed, remediation commands that attach or +detach non-qemu USBIP are prospective only and return +`runtime-capability-unsupported`; they must not be replaced with manual host +USBIP, lock, firewall, or proxy mutations. + | Symptom from `d2b device usb probe` or `d2b guest status` | What it means | Remediation | | --- | --- | --- | | `status=unbound` or `SESSION-CLAIM=missing` | No session owner exists for the declared busid. | `d2b device usb attach corp-vm 1-2 --apply` | -| Attach says the VM is stopped or `guest-import-unavailable` | Guest-control cannot import until the Guest is running. | `d2b guest start corp-vm --apply`, then `d2b device usb attach corp-vm 1-2 --apply` | -| `SESSION-CLAIM=held-by-desired-owner`, `HOST-BIND=bound-to-usbip-host`, and `GUEST=detached` | The host owns and exports the device for this Guest, but guestd has not imported it. | Re-run the row's `d2b device usb attach --apply` command; it converges guest import without releasing the session claim. | +| Attach says the VM is stopped or `guest-import-unavailable` | The target-local USBIP Process cannot import until the Guest is running. | `d2b guest start corp-vm --apply`, then `d2b device usb attach corp-vm 1-2 --apply` | +| `SESSION-CLAIM=held-by-desired-owner`, `HOST-BIND=bound-to-usbip-host`, and `GUEST=detached` | The host owns and exports the device for this Guest, but target-local Process has not imported it. | Re-run the row's `d2b device usb attach --apply` command; it converges guest import without releasing the session claim. | | `SESSION-CLAIM=held-by-other-owner` / `lock-held-by-other-owner` | Another Guest owns the session claim. | `d2b device usb detach 1-2 --apply`, then `d2b device usb attach corp-vm 1-2 --apply` | | `SESSION-CLAIM=stale-owner`, `SESSION-CLAIM=corrupt`, or `invalid-persisted-lock-claim` | The session claim cannot be safely trusted as a healthy owner. | Do not edit the lock file. Run the probe's `command:` line if present; otherwise `d2b device usb detach corp-vm 1-2 --apply`, then `d2b device usb probe`. | | `HOST-BIND=bound-to-unexpected-driver` | The device is present but still owned by another host driver or local application. | Close local consumers of the device, then `d2b device usb attach corp-vm 1-2 --apply`. | diff --git a/docs/how-to/use-console-and-audio.md b/docs/how-to/use-console-and-audio.md index e85e313c25..5a06da2946 100644 --- a/docs/how-to/use-console-and-audio.md +++ b/docs/how-to/use-console-and-audio.md @@ -15,7 +15,7 @@ Confirm with `d2b guest status ` - the audio field appears in the capability summary for audio-enabled VMs. - For `d2b guest console` on an ACA sandbox, the sandbox must be running a - guestd-compatible in-sandbox agent. If it is absent, the daemon + provider relay agent. If it is absent, the daemon returns a typed `provider-misconfigured` error with remediation text; there is no console fallback. - Graphics VMs do not have a serial console surface. Use @@ -75,8 +75,8 @@ The `enforcement` field reflects which side of the policy was applied: - - `host-and-guest` - host and guest enforcement are active (Cloud - Hypervisor NixOS VMs with guestd). + - `host-and-guest` - host and target-local Process enforcement are active + (Cloud Hypervisor NixOS VMs). - `host-only` - only host-side policy is available or applied. - `guest-only` - only guest/provider policy is available or applied. - `unsupported` - the provider does not support guest enforcement. @@ -162,8 +162,8 @@ Collapsed cards show a subtle badge for states that need attention: | Badge | Meaning | Action | | --- | --- | --- | | `host-only` | qemu-media host subset only; guest enforcement unavailable. | Expected; no action required. | -| `provider-misconfigured` | ACA sandbox missing guestd agent. | Deploy the guestd-compatible in-sandbox agent. | -| `degraded` | Cloud Hypervisor guest-side did not apply while host succeeded. | Check guestd status inside the VM. | +| `provider-misconfigured` | ACA sandbox missing provider relay agent. | Deploy the provider-agent-compatible in-sandbox agent. | +| `degraded` | Cloud Hypervisor target-local Process did not apply while host succeeded. | Check the target-local Process status inside the VM. | | `unsupported` | Provider advertises no audio capability. | Verify the VM's audio configuration. | `d2b-wlcontrol` communicates only through the daemon's public socket. diff --git a/docs/how-to/use-persistent-shells.md b/docs/how-to/use-persistent-shells.md index b5377d887d..bff9b04339 100644 --- a/docs/how-to/use-persistent-shells.md +++ b/docs/how-to/use-persistent-shells.md @@ -11,14 +11,11 @@ see [Persistent shell sessions](../explanation/persistent-shells.md). ## Enable persistent shells for a VM -Enable guest control, exec, and shell for a VM with a non-root workload user: +Enable the Guest ComponentSession and shell for a VM: ```nix d2b.vms.work = { - ssh.user = "alice"; - - guest.control.enable = true; - guest.exec.enable = true; + guest.componentSession.enable = true; guest.shell = { enable = true; defaultName = "default"; @@ -28,8 +25,8 @@ d2b.vms.work = { }; ``` -Switch the host configuration, then restart the affected VM so guestd sees the -new shell policy. +Switch the host configuration, then restart the affected VM so the target-local +shell Process sees the new policy. ## Enable an unsafe-local shell diff --git a/docs/how-to/use-usb-security-key.md b/docs/how-to/use-usb-security-key.md index f65c76562a..8a55459cdd 100644 --- a/docs/how-to/use-usb-security-key.md +++ b/docs/how-to/use-usb-security-key.md @@ -29,8 +29,8 @@ When the security-key proxy is active for a VM: - A host configured with d2b v0.4 or later. - A FIDO2-class device connected to the host (YubiKey 5, Security Key NFC, etc.). -- The target VM has `guest.control.enable = true` (required for the guest - frontend DAG node to be supervised). +- The target VM has `guest.componentSession.enable = true` (required for the + Guest frontend Process to be supervised). - You are **not** currently using `usbip.yubikey = true` on the same physical device for the same VM. Proxy and USBIP ownership of the same device are mutually exclusive; see diff --git a/docs/manpages/d2b-host.1 b/docs/manpages/d2b-host.1 index ac65d7de44..6399e76262 100644 --- a/docs/manpages/d2b-host.1 +++ b/docs/manpages/d2b-host.1 @@ -32,7 +32,5 @@ host\-reconcile(1) .TP host\-validate(1) .TP -host\-cutover(1) -.TP host\-help(1) Print this message or the help of the given subcommand(s) diff --git a/docs/plans/2026-08-24-001-refactor-bazel-backed-host-integration-binaries-plan.md b/docs/plans/2026-08-24-001-refactor-bazel-backed-host-integration-binaries-plan.md index 522fca86a5..af280b9250 100644 --- a/docs/plans/2026-08-24-001-refactor-bazel-backed-host-integration-binaries-plan.md +++ b/docs/plans/2026-08-24-001-refactor-bazel-backed-host-integration-binaries-plan.md @@ -171,7 +171,6 @@ Product Contract clarified without scope change: R7, R11, AE2, and F1 bind confi | `broker` | `d2b-priv-broker` | `//packages/d2b-priv-broker:d2b-priv-broker` | | `activationHelper` | `d2b-activation-helper` | `//packages/d2b-host:d2b-activation-helper` | | `hostActivationHelper` | `d2b-host-activation-helper` | `//packages/d2b-host-activation-helper:d2b-host-activation-helper` | -| `cutoverRunner` | `d2b-cutover-runner` | `//packages/d2b-cutover:d2b-cutover-runner` | | `unsafeLocalHelper` | `d2b-unsafe-local-helper` | `//packages/d2b-unsafe-local-helper:d2b-unsafe-local-helper` | | `resourceCompiler` | `d2b-resource-compiler` | `//packages/d2b-resource-compiler:d2b-resource-compiler` | | `waylandProxy` | `d2b-wayland-proxy` | `//packages/d2b-provider-display-wayland:d2b-wayland-proxy` | diff --git a/docs/reference/bazel-buildbuddy.md b/docs/reference/bazel-buildbuddy.md index a74a513ace..a819d3c5ea 100644 --- a/docs/reference/bazel-buildbuddy.md +++ b/docs/reference/bazel-buildbuddy.md @@ -232,7 +232,7 @@ the expected dependency shape without adding a second scheduler or inventory: | Representative change | Query | Expected shape | | --- | --- | --- | -| USBIP Provider implementation | `bazel query 'rdeps(//packages/..., //packages/d2b-provider-device-usbip:d2b_provider_device_usbip)'` | The Provider's tests, `d2bd` composition/final link, and direct `d2b` integration tests only; no `d2bd-runtime`, `d2b-guestd`, sibling Provider, or foundational-contract target. | +| USBIP Provider implementation | `bazel query 'rdeps(//packages/..., //packages/d2b-provider-device-usbip:d2b_provider_device_usbip)'` | The Provider's tests, `d2bd` composition/final link, and direct `d2b` integration tests only; no `d2bd-runtime`, sibling Provider, or foundational-contract target. | | Broker contract | `bazel query 'rdeps(//packages/..., //packages/d2b-contracts-broker:d2b_contracts_broker)'` | Broker/control-plane consumers only; resource-only and desktop interaction Providers remain absent. | | Network Nix surface | `bazel query 'kind("source file", deps(//bazel/checks/nix:nix-unit-network))'` | Only the network surface expression, its selected case, network modules, shared Nix evaluator helpers, and declared tools. Sibling Nix surfaces remain separate labels. | | Documentation-only | `bazel query 'rdeps(//..., //:docs/reference/bazel-buildbuddy.md)'` | Documentation/source-hygiene owners only; no product Rust test or Nix surface target. | diff --git a/docs/reference/broker-w2-dispositions.md b/docs/reference/broker-w2-dispositions.md index a2220ac3a8..f422dacecc 100644 --- a/docs/reference/broker-w2-dispositions.md +++ b/docs/reference/broker-w2-dispositions.md @@ -28,14 +28,10 @@ side-effect audit operation that never reaches the wire dispatcher). | DeletePersistentTap | promoted-live | Resolves the trusted attachment identity and removes the persistent TAP once both generation fences match and the VMM descriptor is closed. | live in production broker | | DeregisterRunnerPidfd | promoted-live | Removes the runner pidfd registry entry idempotently and returns whether an entry was present. | live in production broker | | DiskInit | promoted-live | Resolves trusted disk-init plans for the VM and creates, validates, or safely repairs disk images before runner spawn; ambiguous existing data fails closed. | live in production broker | -| CutoverAudit | promoted-live | Records the admitted cutover runner's durable audit transition through the broker audit chain. | live in production broker | -| CutoverEffect | promoted-live | Executes the closed, capability-bound cutover effect vocabulary and records the resulting disposition. | live in production broker | | ExportBrokerAudit | callable-read-only | Reads the append-only broker audit log, requires `caller_role: AdminUid { uid }`, and streams redacted lines back to `d2bd`. | live read-only callable | -| GuestControlSign | callable-read-only | Computes the per-VM guest-control auth tag over the bound transcript; returns only the transcript-bound MAC tag. | guest-control live callable | | Hello | callable-read-only | Daemon-only handshake; returns `HelloOk` with the broker capability list. | live read-only callable | | InjectSecretById | stubbed-unimplemented | Returns `BrokerError::Unimplemented`; secret write paths are not implemented. | future work | | LaunchMinijailChild | stubbed-unimplemented | Returns `BrokerError::Unimplemented`; privileged child launch is not implemented. | future work | -| LaunchCutoverRunner | promoted-live | Admits the one-shot cutover runner with one bootstrap descriptor and a capability-bound operation registry entry. | live in production broker | | ModprobeIfAllowed | promoted-live | Resolves the trusted module policy, checks the host module posture, and runs the live modprobe handler when allowed. | live in production broker | | MigrateLegacySwtpmState | promoted-live | Resolves the trusted legacy swtpm intent and performs crash-safe, byte-preserving journal/marker migration with replay and source retirement. | live in production broker | | ObserveRunner | promoted-live | Re-discovers a declared runner and returns bounded process identity and cgroup verification metadata. | live in production broker | diff --git a/docs/reference/cgroup-delegation.md b/docs/reference/cgroup-delegation.md index 5244602915..147f3d89a8 100644 --- a/docs/reference/cgroup-delegation.md +++ b/docs/reference/cgroup-delegation.md @@ -52,7 +52,7 @@ split into two categories: The host systemd posture is part of that contract: `d2b.slice` is delegated by systemd with the `cpu`, `memory`, `pids`, `io`, and `cpuset` controllers, and -`d2b-priv-broker.service` runs inside that slice with +`d2b-broker.service` runs inside that slice with `Delegate=true` and `KillMode=process`. Broker restarts therefore stop only the broker process; runner teardown stays on the audited pidfd/`CgroupKill` path instead of a service-wide systemd kill. @@ -158,7 +158,7 @@ cpuset enable is the only one with a preceding inheritance step. ## Fd ownership table The daemon owns one `Arc` per registered pidfd in -[`d2b_priv_broker` → +[`d2b_broker` → `d2bd_runtime::supervisor::pidfd::PidfdTable`](../../packages/d2bd-runtime/src/supervisor/pidfd.rs). | Layer | What it holds | Lifetime | @@ -195,7 +195,7 @@ path-safety contract: never from caller input. The canary matrices in `packages/d2b-host/src/cgroup.rs` and -`packages/d2b-priv-broker/src/ops/cgroup.rs` exercise the refusal paths. Both +`packages/d2b-broker/src/ops/cgroup.rs` exercise the refusal paths. Both are included in `test-rust`. ## Audit records diff --git a/docs/reference/cli-contract.md b/docs/reference/cli-contract.md index d5342fd344..be654c31dc 100644 --- a/docs/reference/cli-contract.md +++ b/docs/reference/cli-contract.md @@ -73,8 +73,8 @@ fallback. When `--item` is omitted, the CLI selects `defaultItem`, then an only item, otherwise returns the available item ids and names. For local-VM exec items, d2bd derives an opaque guest exec id from the -authenticated requester, operation id, target, and item id. Guestd persists that -id with the detached exec record, so replay after a daemon restart returns the +authenticated requester, operation id, target, and item id. The target-local +Process persists that id with the detached exec record, so replay after a daemon restart returns the existing exec instead of spawning a duplicate. A replay whose trusted argv hash does not match fails closed. @@ -350,13 +350,13 @@ targets route through the configured gateway entrypoint when supported. The realm target grammar is `.[....].d2b`. Bare local VM names stay on the -existing host fast path until the runtime/Nix cutover lands. Fully qualified +existing host fast path until the runtime/Nix transition lands. Fully qualified realm targets must resolve through the realm access layer; missing entrypoints fail closed with an actionable `missing-realm-entrypoint` error rather than falling back to SSH or a generic tunnel. `d2b guest list --zone ` runs `d2b guest list` against the -realm gateway through the same local guest-control exec path. It does not +realm gateway through the same local component-session exec path. It does not make the host persist a remote node/workload registry. ### `vm display` @@ -768,22 +768,22 @@ Host and bridge diagnostics are a host surface, not a Guest Resource status. ### `device usb attach` -**Synopsis:** `d2b device usb attach [--dry-run | --apply] [--human] [--json]` +**Synopsis:** `d2b device usb attach [--dry-run | --apply] [--human] [--json]` **Flags** | Flag | Type | Default | Semantics | | --- | --- | --- | --- | -| `--dry-run` | boolean | `false` | Print the daemon → broker USBIP attach plan plus the authenticated guestd import step without mutating host or guest state. | -| `--apply` | boolean | `false` | Ask `d2bd` to run three fail-closed pre-flight checks (sysfs presence, USB-capable gate, active claim exclusivity), then dispatch the appropriate broker path: **declared path** (when a static bundle intent exists for the busid - `UsbipBind` + firewall carve-out + `UsbipProxyReconcile`), or **explicit path** (when no declared intent exists - `UsbipExplicitFirewallRule` + `UsbipExplicitBind` per-device ops), then ask guestd over authenticated guest-control to import the selected busid. | -| `--json` | boolean | `false` | Emit the dry-run summary as structured JSON. | -| `--human` | boolean | `false` | Force the human dry-run summary on stdout. | +| `--dry-run` | boolean | `false` | Send a typed Resource API intent. Non-qemu USBIP requests report `runtime-capability-unsupported`; they do not claim a host or Guest plan. | +| `--apply` | boolean | `false` | Require the Device resource to resolve to the USBIP Provider and require the signed target-local Guest Process/ComponentSession import path. Until that path is wired, fail closed before host bind, proxy reconciliation, or claim mutation. | +| `--json` | boolean | `false` | Emit the daemon response as structured JSON. | +| `--human` | boolean | `false` | Force human-readable daemon output. | **Arguments** | Argument | Semantics | | --- | --- | -| `vm` | Required VM name. | +| `device` | Required `Device/` ResourceRef. | | `busid` | Required host USB busid in the canonical `B-P[.P...]` form (for example `1-2` or `2-1.4`). Does not require the busid to be pre-declared in the NixOS bundle configuration. | **Exit codes** @@ -793,65 +793,28 @@ Host and bridge diagnostics are a host surface, not a Guest Resource status. | `0` | Success. | - | | `1` | `d2bd` is unreachable, or the daemon returned a non-typed USBIP failure. | [`daemon-down`](./error-codes.md#daemon-down) | | `2` | Missing VM / busid or another usage error. | [`usage`](./error-codes.md#usage) | -| `67` | The USB device busid is not present in sysfs (`usbip-busid-not-present`), or another VM already holds an active claim on this busid (`usbip-explicit-claim-conflict`). | [`usbip-busid-not-present`](./error-codes.md#usbip-busid-not-present), [`usbip-explicit-claim-conflict`](./error-codes.md#usbip-explicit-claim-conflict) | -| `78` | The daemon reached the broker but the native USBIP apply path was refused. | [`broker-error`](./error-codes.md#broker-error) | +| `70` | The target-local USBIP Process or authenticated Guest import path is unavailable. | [`runtime-capability-unsupported`](./error-codes.md#runtime-capability-unsupported) | **Human example** ```text -$ d2b device usb attach corp-vm 1-2 --dry-run -d2b device usb attach --dry-run: would bind and lock, apply the USBIP firewall carve-out, ensure the per-env backend/proxy for busid '1-2' for vm 'corp-vm', reconcile the USBIP proxy, and ask guestd to import the device -``` - -**Explicit attach (present-busid, no static allowlist required)** - -`d2b device usb attach` accepts any USB device that is physically present in sysfs -without requiring a static busid or vendor allowlist in the NixOS configuration. -The daemon selects the **explicit path** when no declared bundle intent exists for -the requested busid. Three fail-closed checks run before any broker call: - -1. **Sysfs presence** - the daemon checks `/sys/bus/usb/devices//idVendor`. - If absent, the attach fails with `UsbipBusidNotPresent` (exit 67) and guides - the operator to plug in the device before retrying. -2. **USB-capable gate** - the VM must have `RuntimeCapabilityGate::UsbHotplug` - declared in its manifest. Non-USB-capable VMs fail with a typed - `RuntimeCapabilityUnsupported` error. -3. **Active claim exclusivity** - the daemon reads the OFD lock under - `/run/d2b/locks/usbip/`. If another VM already holds the claim, the - attach fails with `UsbipExplicitClaimConflict` (exit 67) naming the owner VM - and guiding the operator to detach from the owner first. - -The explicit path dispatches `UsbipExplicitFirewallRule` (env-scoped nftables -rule keyed on the per-env uplink IPs) and `UsbipExplicitBind` (per-device backend -setup). Both ops are currently typed stubs; the live per-device backend handler -is deferred to later implementation. The declared path (static bundle intents) is -unaffected. +$ d2b device usb attach Device/corp-vm 1-2 --dry-run +d2b device usb attach: runtime capability 'usbip-guest-import' is unavailable; no host USBIP mutation was performed +``` **Status** -The native CLI sends one intent to `d2bd`; the daemon drives broker host -USBIP state and authenticated guestd import cleanup/attach over guest-control. -If the target VM is stopped, `--apply` fails before host mutation with an -actionable usage error: start the Guest with -`d2b guest start --apply`, wait until it is Ready, then retry -`d2b device usb attach --apply`. This preflight does not create a -degraded USB state. If an earlier failed apply left a stale or bound USBIP -session claim, start the VM and rerun the attach or run -`d2b device usb detach --apply`; the attach/detach paths and -`d2b device usb probe` all run the USBIP proxy reconcile pass, and `device usb probe` -shows the session claim as cleared once the lock/proxy state is consistent. - -Prerequisites for `--apply` are: the target VM is running and guest-control -advertises USBIP status/import, the bundle declares a USBIP bind/firewall intent -for the VM/busid, policy/topology checks allow the physical device, the -`usbip-host` module and per-env backend/proxy carrier can be prepared, and no -other owner holds the busid session claim. Failing prerequisites surface as -typed errors or as `d2b device usb probe` degraded reasons with exact remediation -commands. +The native CLI sends one Device Resource API intent to `d2bd`. For non-qemu +USBIP, the daemon currently returns `runtime-capability-unsupported` before +host bind, firewall/proxy mutation, or claim release because no signed +target-local USBIP Process effect adapter is wired. `d2b device usb probe` +remains observational and reports Guest import as unavailable. Qemu-media +hotplug remains a separate runtime path. **Native** -- `--apply` routes through `d2bd` → broker + guestd. There is no SSH fallback for USBIP. +- `--apply` routes through the typed Device Resource API. There is no SSH or + guestd fallback for USBIP. **Bash** @@ -859,22 +822,22 @@ commands. ### `device usb detach` -**Synopsis:** `d2b device usb detach [--dry-run | --apply] [--human] [--json]` +**Synopsis:** `d2b device usb detach [--dry-run | --apply] [--human] [--json]` **Flags** | Flag | Type | Default | Semantics | | --- | --- | --- | --- | -| `--dry-run` | boolean | `false` | Print the daemon → broker USBIP unbind plan without mutating host state. | -| `--apply` | boolean | `false` | Ask `d2bd` to run `UsbipUnbind` followed by `UsbipProxyReconcile` for the selected VM/busid pair. | -| `--json` | boolean | `false` | Emit the dry-run summary as structured JSON. | -| `--human` | boolean | `false` | Force the human dry-run summary on stdout. | +| `--dry-run` | boolean | `false` | Send a typed Resource API intent. Non-qemu USBIP requests report `runtime-capability-unsupported` without claiming a host teardown plan. | +| `--apply` | boolean | `false` | Require the signed target-local Guest Process/ComponentSession detach path. Until that path is wired, fail closed before host unbind, proxy reconciliation, or claim release. | +| `--json` | boolean | `false` | Emit the daemon response as structured JSON. | +| `--human` | boolean | `false` | Force human-readable daemon output. | **Arguments** | Argument | Semantics | | --- | --- | -| `vm` | Required VM name. | +| `device` | Required `Device/` ResourceRef. | | `busid` | Required host USB busid in the canonical `B-P[.P...]` form. | **Exit codes** @@ -884,33 +847,33 @@ commands. | `0` | Success. | - | | `1` | `d2bd` is unreachable, or the daemon returned a non-typed USBIP failure. | [`daemon-down`](./error-codes.md#daemon-down) | | `2` | Missing VM / busid or another usage error. | [`usage`](./error-codes.md#usage) | -| `78` | The daemon reached the broker but the native USBIP apply path was refused. | [`broker-error`](./error-codes.md#broker-error) | +| `70` | The target-local USBIP Process or authenticated Guest detach path is unavailable. | [`runtime-capability-unsupported`](./error-codes.md#runtime-capability-unsupported) | **Human example** ```text -$ d2b device usb detach corp-vm 1-2 --dry-run -d2b device usb detach --dry-run: would ask guestd to detach busid '1-2' for vm 'corp-vm', unbind it on the host, and reconcile the USBIP proxy +$ d2b device usb detach Device/corp-vm 1-2 --dry-run +d2b device usb detach: runtime capability 'usbip-guest-import' is unavailable; no host USBIP mutation was performed ``` **Status** -The native CLI first asks guestd to detach matching imports, then drives the -daemon → broker `UsbipUnbind` / `UsbipProxyReconcile` path. Explicit detach is -the only normal path that releases a USBIP session claim. VM stop/restart keeps -the claim for the same VM within the current host boot/session so restart -reconciliation can re-import the device; a host reboot clears the `/run` lock. +The native CLI sends one Device Resource API intent to `d2bd`. For non-qemu +USBIP, the daemon currently returns `runtime-capability-unsupported` before +Guest detach or host unbind because no signed target-local USBIP Process effect +adapter is wired. The claim is retained for safe retry; a host reboot still +clears the `/run` lock. -Single-busid detach never stops the shared per-env proxy. If the daemon cannot -prove firewall-withdrawal-before-flow-kill ordering plus an exact VM/proxy -cleanup tuple, `--apply` fails closed with a revocation-isolation error and -preserves the session claim for manual drain/recovery. The public error names the -target busid. The safe next step is to stop the VM so the stream drains, then -rerun `d2b device usb detach --apply`. +When the Guest effect path is implemented, single-busid detach must never stop +the shared per-env proxy. If the daemon cannot prove +firewall-withdrawal-before-flow-kill ordering plus an exact VM/proxy cleanup +tuple, `--apply` must fail closed and preserve the session claim for manual +drain/recovery. **Native** -- `--apply` routes through `d2bd` → broker `UsbipUnbind` then `UsbipProxyReconcile`. +- `--apply` routes through the typed Device Resource API. There is no SSH or + guestd fallback for USBIP. **Bash** @@ -940,7 +903,7 @@ rerun `d2b device usb detach --apply`. | `0` | Success. | - | | `1` | `d2bd` is unreachable or does not expose the native USBIP probe request. | [`daemon-down`](./error-codes.md#daemon-down) | | `2` | Unsupported invocation shape. | [`usage`](./error-codes.md#usage) | -| `78` | The daemon reached the broker but the `UsbipProxyReconcile` pass failed. | [`broker-error`](./error-codes.md#broker-error) | +| `78` | The daemon could not refresh an independent qemu-media registry. | [`broker-error`](./error-codes.md#broker-error) | **Human example** @@ -948,9 +911,8 @@ rerun `d2b device usb detach --apply`. $ d2b device usb probe VM ENV BUSID STATUS SESSION-CLAIM HOST-BIND CARRIER PROXY GUEST POLICY corp-vm work 1-2 degraded held-by-desired-owner unknown unknown unknown detached allowed - degraded guest-import-unavailable: the guest USBIP import has not converged - remediation: Run `d2b device usb attach corp-vm 1-2 --apply` after the VM is running. - command: d2b device usb attach corp-vm 1-2 --apply + degraded guest-import-unavailable: Guest USB import requires a target-local USBIP Process path that is unavailable; the host claim is not a successful attachment. + remediation: Use a build that includes the signed target-local USBIP Process over ComponentSession, then retry the command. ``` **`--json` example** - schema: [`usb-probe.schema.json`](./cli-output/usb-probe.schema.json); prose companion: [`usb-probe.md`](./cli-output/usb-probe.md). @@ -976,7 +938,7 @@ corp-vm work 1-2 degraded held-by-desired-ow "proxy": "unknown" }, "guest": { - "import": "detached" + "import": "unavailable" }, "topologyPolicy": { "topology": "unknown", @@ -985,13 +947,11 @@ corp-vm work 1-2 degraded held-by-desired-ow "degradedReasons": [ { "code": "guest-import-unavailable", - "summary": "the guest USBIP import has not converged", - "remediation": "Run `d2b device usb attach corp-vm 1-2 --apply` after the VM is running." + "summary": "Guest USB import requires a target-local USBIP Process path that is unavailable; the host claim is not a successful attachment.", + "remediation": "Use a build that includes the signed target-local USBIP Process over ComponentSession, then retry the command." } ], - "remediationCommands": [ - "d2b device usb attach corp-vm 1-2 --apply" - ] + "remediationCommands": [] } ] } @@ -999,8 +959,8 @@ corp-vm work 1-2 degraded held-by-desired-ow **Status** -Probe is a read-only daemon RPC backed by the broker's -`UsbipProxyReconcile` validation pass. The JSON and human forms split: +Probe is a read-only daemon RPC. It does not reconcile or mutate the host +proxy for non-qemu USBIP. The JSON and human forms split: - the **session claim** (`missing`, `held-by-desired-owner`, `held-by-other-owner`, `stale-owner`, `corrupt`, or `not-applicable`); @@ -1021,13 +981,11 @@ reconciled. Public output uses redacted state labels and summaries rather than raw sysfs paths, raw serials, stderr, or policy internals. VM restart reconciliation preserves same-VM session claims for the current host -boot/session, detaches guest imports, and only runs host unbind after firewall -withdrawal plus targeted stream cleanup is proven. It then replays host -bind/proxy and guest import after guest-control readiness. Runtime absence, -proxy unavailability, or guest import unavailability surfaces as degraded USB -state. Required policy/topology failures fail before device exposure and must be -remediated by fixing the declaration, rebuilding, and rerunning the lifecycle -command. +boot/session. Until the signed target-local USBIP Process path is available, +start and detach stop before host release, while Guest import unavailability +surfaces as degraded USB state. Required policy/topology failures fail before +device exposure and must be remediated by fixing the declaration, rebuilding, +and rerunning the lifecycle command. **Native** @@ -1243,7 +1201,7 @@ until the daemon handler ships. | `0` | Success. | - | | `1` | Console launch or output read failure. | [`generic`](./error-codes.md#generic) | | `2` | Unknown VM, missing argument, or graphics VM selected. | [`usage`](./error-codes.md#usage) | -| `80` | `provider-misconfigured`: ACA sandbox without an active guestd-compatible console transport; see [ACA console - provider misconfiguration](./provider-capability-matrix.md#aca-console--provider-misconfiguration). | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | +| `80` | `provider-misconfigured`: ACA sandbox without an active target-local Process-compatible console transport; see [ACA console - provider misconfiguration](./provider-capability-matrix.md#aca-console--provider-misconfiguration). | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | **Human example** @@ -1258,7 +1216,7 @@ output. **Status** -The Rust CLI dispatches `ConsoleOp` to `d2bd` over the public socket. The daemon owns a persistent ring-buffer drainer per VM and hands the attached operator session reads from that buffer. Provider-capability resolution runs before attach; ACA targets without an active guestd-compatible terminal transport surface a typed `provider-misconfigured` error (exit `80`) rather than falling back to any shell channel. See [provider capability matrix](./provider-capability-matrix.md) for the per-provider transport model; the design is governed by [ADR 0041](../adr/0041-console-and-audio-controls.md) and [ADR 0015](../adr/0015-daemon-only-clean-break.md). +The Rust CLI dispatches `ConsoleOp` to `d2bd` over the public socket. The daemon owns a persistent ring-buffer drainer per VM and hands the attached operator session reads from that buffer. Provider-capability resolution runs before attach; ACA targets without an active target-local Process-compatible terminal transport surface a typed `provider-misconfigured` error (exit `80`) rather than falling back to any shell channel. See [provider capability matrix](./provider-capability-matrix.md) for the per-provider transport model; the design is governed by [ADR 0041](../adr/0041-console-and-audio-controls.md) and [ADR 0015](../adr/0015-daemon-only-clean-break.md). **Native** @@ -1291,7 +1249,7 @@ The Rust CLI dispatches `ConsoleOp` to `d2bd` over the public socket. The daemon | `0` | Success. Per-target `enforcement: unsupported` (e.g. qemu-media guest-side) is reported in the output body, not as an error exit. | - | | `1` | Unexpected filesystem or state probe failure. | [`generic`](./error-codes.md#generic) | | `2` | Unknown VM or unsupported invocation shape. | [`usage`](./error-codes.md#usage) | -| `80` | `provider-misconfigured`: ACA sandbox without an active guestd audio transport; see [provider capability matrix](./provider-capability-matrix.md#aca-audio). | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | +| `80` | `provider-misconfigured`: ACA sandbox without an active target-local Process audio transport; see [provider capability matrix](./provider-capability-matrix.md#aca-audio). | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | **Human example** @@ -1306,7 +1264,7 @@ device: detached **Status** -The Rust CLI dispatches `AudioOp::GetState` to `d2bd` over the public socket. Provider capability resolution runs before any state access; Cloud Hypervisor NixOS VMs read OFD-locked state from `/run/d2b/audio/.json`, qemu-media VMs report `enforcement: unsupported` for guest-side, and ACA sandbox VMs route through provider guestd. Multi-target queries return per-target errors so one misconfigured provider does not fail the entire response. See [provider capability matrix](./provider-capability-matrix.md) for the per-provider enforcement model. +The Rust CLI dispatches `AudioOp::GetState` to `d2bd` over the public socket. Provider capability resolution runs before any state access; Cloud Hypervisor NixOS VMs read OFD-locked state from `/run/d2b/audio/.json`, qemu-media VMs report `enforcement: unsupported` for guest-side, and ACA sandbox VMs route through provider target-local Process. Multi-target queries return per-target errors so one misconfigured provider does not fail the entire response. See [provider capability matrix](./provider-capability-matrix.md) for the per-provider enforcement model. **Native** @@ -1340,7 +1298,7 @@ The Rust CLI dispatches `AudioOp::GetState` to `d2bd` over the public socket. Pr | `0` | Success. State is persisted and enforcement applied; `applied: host-only` is reported only for providers such as qemu-media where guest enforcement is unsupported. | - | | `1` | Audio state write, sidecar, or hotplug failure. | [`generic`](./error-codes.md#generic) | | `2` | Bad state literal, unknown VM, or audio not enabled for the VM. | [`usage`](./error-codes.md#usage) | -| `80` | `provider-misconfigured`: ACA sandbox without an active guestd audio transport. | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | +| `80` | `provider-misconfigured`: ACA sandbox without an active target-local Process audio transport. | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | **Human example** @@ -1357,7 +1315,7 @@ device: will-attach-on-next-up **Status** -The Rust CLI dispatches `AudioOp::SetMic` to `d2bd` over the public socket. The daemon writes OFD-locked state atomically and, where guest enforcement is supported, applies the guest mic grant via guestd before reporting `host-and-guest`. Providers without guest enforcement, such as qemu-media, report the explicit `host-only` posture. See [provider capability matrix](./provider-capability-matrix.md) for the per-provider enforcement model. +The Rust CLI dispatches `AudioOp::SetMic` to `d2bd` over the public socket. The daemon writes OFD-locked state atomically and, where guest enforcement is supported, applies the guest mic grant via target-local Process before reporting `host-and-guest`. Providers without guest enforcement, such as qemu-media, report the explicit `host-only` posture. See [provider capability matrix](./provider-capability-matrix.md) for the per-provider enforcement model. **Native** @@ -1391,7 +1349,7 @@ The Rust CLI dispatches `AudioOp::SetMic` to `d2bd` over the public socket. The | `0` | Success. State is persisted and enforcement applied; `applied: host-only` is reported only for providers such as qemu-media where guest enforcement is unsupported. | - | | `1` | Audio state write, sidecar, or hotplug failure. | [`generic`](./error-codes.md#generic) | | `2` | Bad state literal, unknown VM, or audio not enabled for the VM. | [`usage`](./error-codes.md#usage) | -| `80` | `provider-misconfigured`: ACA sandbox without an active guestd audio transport. | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | +| `80` | `provider-misconfigured`: ACA sandbox without an active target-local Process audio transport. | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | **Human example** @@ -1441,7 +1399,7 @@ The Rust CLI dispatches `AudioOp::SetSpeaker` to `d2bd` over the public socket. | `0` | Success. Calling the command against a VM that never had audio enabled is an idempotent no-op. `applied: host-only` is reported only for providers such as qemu-media where guest enforcement is unsupported. | - | | `1` | Audio state write or sidecar failure. | [`generic`](./error-codes.md#generic) | | `2` | Missing or unknown VM name. | [`usage`](./error-codes.md#usage) | -| `80` | `provider-misconfigured`: ACA sandbox without an active guestd audio transport. | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | +| `80` | `provider-misconfigured`: ACA sandbox without an active target-local Process audio transport. | [`provider-misconfigured`](./error-codes.md#provider-misconfigured) | **Human example** @@ -1521,14 +1479,14 @@ Build is a native non-destructive planner that renders the eval/build preview wi `activation switch` is a daemon-native live Guest activation verb. If neither mutation flag is set, the CLI prints the parity notice and defaults to `--dry-run`; `--apply` requires the VM to be running and reachable over -guest-control. +component-session. **Flags** | Flag | Type | Default | Semantics | | --- | --- | --- | --- | | `--dry-run` | boolean | implicit if neither mutation flag is set | Plan the activation without mutating the guest. | -| `--apply` | boolean | `false` | Publish the prepared VM closure into the VM's live store pool, ask guestd to activate that prepared toplevel inside the running guest, poll guest activation status, then commit the successful generation through the broker. | +| `--apply` | boolean | `false` | Publish the prepared VM closure into the VM's live store pool, ask target-local Process to activate that prepared toplevel inside the running guest, poll guest activation status, then commit the successful generation through the broker. | | `--json` | boolean | `false` | Emit the dry-run summary or typed mutating-verb envelope as JSON. | | `--human` | boolean | `false` | Force the human summary on stdout. | @@ -1543,7 +1501,7 @@ guest-control. | Code | Meaning | Typed error / reference | | --- | --- | --- | | `0` | Dry-run plan rendered or `--apply` completed successfully. | - | -| `1` | `d2bd` is unreachable or guest-control transport fails before a typed activation result is available. | [`daemon-down`](./error-codes.md#daemon-down), [`generic`](./error-codes.md#generic) | +| `1` | `d2bd` is unreachable or component-session transport fails before a typed activation result is available. | [`daemon-down`](./error-codes.md#daemon-down), [`generic`](./error-codes.md#generic) | | `2` | Unknown flag or unsupported invocation shape. | [`usage`](./error-codes.md#usage) | | `70` | The named VM is not declared in the active manifest. | [`not-found`](./error-codes.md#not-found) | | `78` | Host store publication, broker commit, guest activation capability, or guest activation status failed closed. | [`broker-error`](./error-codes.md#broker-error), [`not-yet-implemented`](./error-codes.md#not-yet-implemented) | @@ -1573,8 +1531,8 @@ Guest/corp-vm: activation=applied **Native** - `--apply`: routes through `d2bd`, which prepares/publishes the - closure, opens the authenticated guest-control activation flow, waits - for guestd status, and only then asks the broker to commit host-side + closure, opens the authenticated component-session activation flow, waits + for target-local Process status, and only then asks the broker to commit host-side generation metadata. Successful commits publish both legacy activation metadata and split store-view `state/current` / `meta/current` pointers. Daemon-unreachable surfaces `daemon-down` exit 1; @@ -1601,7 +1559,7 @@ mutation flag is set, the CLI prints the parity notice and defaults to | Flag | Type | Default | Semantics | | --- | --- | --- | --- | | `--dry-run` | boolean | implicit if neither mutation flag is set | Plan the offline staging update without mutating guest or host generation state. | -| `--apply` | boolean | `false` | Publish the prepared VM closure into the VM's live store pool and commit it as the toplevel to use on the next VM start; no guest-control activation is attempted. | +| `--apply` | boolean | `false` | Publish the prepared VM closure into the VM's live store pool and commit it as the toplevel to use on the next VM start; no component-session activation is attempted. | | `--json` | boolean | `false` | Emit the dry-run summary or typed mutating-verb envelope as JSON. | | `--human` | boolean | `false` | Force the human summary on stdout. | @@ -1624,7 +1582,7 @@ mutation flag is set, the CLI prints the parity notice and defaults to `boot --apply` is the explicit way to stage a new toplevel while the VM is stopped/offline. It only changes the host-side generation selected for the next start; it does not run guest activation in the current -guest and does not require guest-control capability. +guest and does not require component-session capability. **Human example** @@ -1636,7 +1594,7 @@ d2b boot --apply staged next-boot toplevel (vm=corp-vm, mode=boot, summary=stage **Native** - `--apply`: routes through `d2bd` and the broker-backed store - publication/commit path only. It intentionally skips guest-control + publication/commit path only. It intentionally skips component-session activation because there may be no running guest. - The `D2B_NATIVE_ONLY=1` / `D2B_LEGACY_BASH_OPT_IN=1` env vars were retired; the daemon-only contract is the only path. @@ -1653,14 +1611,14 @@ d2b boot --apply staged next-boot toplevel (vm=corp-vm, mode=boot, summary=stage `test` is a daemon-native live guest activation verb for temporary activation until reboot. If neither mutation flag is set, the CLI prints the parity notice and defaults to `--dry-run`; `--apply` requires the VM -to be running and reachable over guest-control. +to be running and reachable over component-session. **Flags** | Flag | Type | Default | Semantics | | --- | --- | --- | --- | | `--dry-run` | boolean | implicit if neither mutation flag is set | Plan the activation without mutating the guest. | -| `--apply` | boolean | `false` | Publish the prepared VM closure into the VM's live store pool, ask guestd to run a test activation of that prepared toplevel inside the running guest, poll guest activation status, then commit the successful host-side metadata needed to observe the temporary generation. | +| `--apply` | boolean | `false` | Publish the prepared VM closure into the VM's live store pool, ask target-local Process to run a test activation of that prepared toplevel inside the running guest, poll guest activation status, then commit the successful host-side metadata needed to observe the temporary generation. | | `--json` | boolean | `false` | Emit the dry-run summary or typed mutating-verb envelope as JSON. | | `--human` | boolean | `false` | Force the human summary on stdout. | @@ -1675,26 +1633,26 @@ to be running and reachable over guest-control. | Code | Meaning | Typed error / reference | | --- | --- | --- | | `0` | Dry-run plan rendered or `--apply` completed successfully. | - | -| `1` | `d2bd` is unreachable or guest-control transport fails before a typed activation result is available. | [`daemon-down`](./error-codes.md#daemon-down), [`generic`](./error-codes.md#generic) | +| `1` | `d2bd` is unreachable or component-session transport fails before a typed activation result is available. | [`daemon-down`](./error-codes.md#daemon-down), [`generic`](./error-codes.md#generic) | | `2` | Unknown flag or unsupported invocation shape. | [`usage`](./error-codes.md#usage) | | `70` | The named VM is not declared in the active manifest. | [`not-found`](./error-codes.md#not-found) | | `78` | Host store publication, broker commit, guest activation capability, or guest activation status failed closed. | [`broker-error`](./error-codes.md#broker-error), [`not-yet-implemented`](./error-codes.md#not-yet-implemented) | Stopped/offline VMs fail closed for `test --apply`. Use `boot --apply` -for offline staging, or start the VM and retry once guest-control +for offline staging, or start the VM and retry once component-session advertises activation support. **Human example** ```text $ d2b test corp-vm --apply -d2b test --apply activated in guest via guest-control (vm=corp-vm, mode=test, summary=activated until reboot, generationNumber=42) +d2b test --apply activated in guest via component-session (vm=corp-vm, mode=test, summary=activated until reboot, generationNumber=42) ``` **Native** -- `--apply`: routes through `d2bd`, guest-control activation, and a - broker commit after guestd reports success. There is no host-side +- `--apply`: routes through `d2bd`, component-session activation, and a + broker commit after target-local Process reports success. There is no host-side execution of guest activation scripts. - The `D2B_NATIVE_ONLY=1` / `D2B_LEGACY_BASH_OPT_IN=1` env vars were retired; the daemon-only contract is the only path. @@ -1711,14 +1669,14 @@ d2b test --apply activated in guest via guest-control (vm=corp-vm, mode=test, su `rollback` is a daemon-native live guest rollback verb. If neither mutation flag is set, the CLI prints the parity notice and defaults to `--dry-run`; live `--apply` requires the VM to be running and reachable -over guest-control. +over component-session. **Flags** | Flag | Type | Default | Semantics | | --- | --- | --- | --- | | `--dry-run` | boolean | implicit if neither mutation flag is set | Plan the activation without mutating the guest. | -| `--apply` | boolean | `false` | Select the previous prepared VM toplevel, ensure it is published into the VM's live store pool, ask guestd to activate it inside the running guest, poll guest activation status, then commit the successful rollback generation through the broker. | +| `--apply` | boolean | `false` | Select the previous prepared VM toplevel, ensure it is published into the VM's live store pool, ask target-local Process to activate it inside the running guest, poll guest activation status, then commit the successful rollback generation through the broker. | | `--json` | boolean | `false` | Emit the dry-run summary or typed mutating-verb envelope as JSON. | | `--human` | boolean | `false` | Force the human summary on stdout. | @@ -1733,7 +1691,7 @@ over guest-control. | Code | Meaning | Typed error / reference | | --- | --- | --- | | `0` | Dry-run plan rendered or `--apply` completed successfully. | - | -| `1` | `d2bd` is unreachable or guest-control transport fails before a typed activation result is available. | [`daemon-down`](./error-codes.md#daemon-down), [`generic`](./error-codes.md#generic) | +| `1` | `d2bd` is unreachable or component-session transport fails before a typed activation result is available. | [`daemon-down`](./error-codes.md#daemon-down), [`generic`](./error-codes.md#generic) | | `2` | Unknown flag or unsupported invocation shape. | [`usage`](./error-codes.md#usage) | | `70` | The named VM is not declared in the active manifest. | [`not-found`](./error-codes.md#not-found) | | `78` | Host store publication, broker commit, guest activation capability, or guest activation status failed closed. | [`broker-error`](./error-codes.md#broker-error), [`not-yet-implemented`](./error-codes.md#not-yet-implemented) | @@ -1747,13 +1705,13 @@ run guest activation from the host. ```text $ d2b rollback corp-vm --apply -d2b rollback --apply activated previous toplevel in guest via guest-control (vm=corp-vm, mode=rollback, summary=rolled back, generationNumber=41) +d2b rollback --apply activated previous toplevel in guest via component-session (vm=corp-vm, mode=rollback, summary=rolled back, generationNumber=41) ``` **Native** -- `--apply`: routes through `d2bd`, guest-control activation, and a - broker commit after guestd reports success. There is no host-side +- `--apply`: routes through `d2bd`, component-session activation, and a + broker commit after target-local Process reports success. There is no host-side execution of guest activation scripts. - The `D2B_NATIVE_ONLY=1` / `D2B_LEGACY_BASH_OPT_IN=1` env vars were retired; the daemon-only contract is the only path. @@ -2145,7 +2103,7 @@ Keys show is a native preview that reports daemon-resolved key metadata placehol | Flag | Type | Default | Semantics | | --- | --- | --- | --- | -| `--dry-run` | boolean | implicit if neither mutation flag is set | Plan the key rotation without changing managed host keys or guest auth state. | +| `--dry-run` | boolean | implicit if neither mutation flag is set | Plan the key rotation without changing managed host keys or session material. | | `--apply` | boolean | `false` | Perform the managed-key rotation. | | `--json` | boolean | `false` | Emit the dry-run summary or typed mutating-verb envelope as JSON. | | `--human` | boolean | `false` | Force the human summary on stdout. | @@ -2458,104 +2416,6 @@ host destroy --dry-run: no d2b-owned resources to remove - In v1.0 daemon-only, `exec_legacy_passthrough` always returns the typed `not-yet-implemented` envelope (exit 78 per ADR 0015); the historical bash-fallback shim was retired in v1.0. -### `host migrate-storage` - -**Synopsis:** `d2b host migrate-storage [--dry-run | --apply | --rollback --from-checkpoint ] [--human | --json]` - -**Status** - -Plans the one-time breaking storage layout cutover. The current build is -read-only for this verb: `--dry-run` emits a checkpoint ID, the exact -rollback command, preflight requirements, preserved persistent data, -cutover-only cleanup candidates, and fail-closed hazards. `--apply` and -`--rollback` fail closed until the broker-backed mover ships. - -**Flags** - -| Flag | Type | Default | Semantics | -| --- | --- | --- | --- | -| `--dry-run` | boolean | required unless `--apply` or `--rollback` is set | Plan the storage cutover without moving or deleting host state. | -| `--apply` | boolean | `false` | Apply the storage cutover. Currently returns a typed not-implemented envelope. | -| `--rollback` | boolean | `false` | Roll back from a checkpoint. Currently returns a typed not-implemented envelope. | -| `--from-checkpoint` | string | required with `--rollback` | Checkpoint ID from the dry-run plan. | -| `--json` | boolean | `false` | Emit the dry-run plan or typed refusal envelope as JSON. | -| `--human` | boolean | `false` | Emit the human dry-run plan. | - -**Arguments** - -| Argument | Semantics | -| --- | --- | -| _(none)_ | Storage cutover planning is global. | - -**Dry-run JSON shape** - -```json -{ - "command": "host migrate-storage", - "mode": "dry-run", - "checkpointId": "storage-cutover-…", - "rollbackCommand": "d2b host migrate-storage --rollback --from-checkpoint storage-cutover-…", - "vmCount": 2, - "vms": ["corp-vm", "work-vm"], - "preflightRequirements": [ - "all d2b VMs stopped", - "d2bd.service stopped", - "d2b-priv-broker.service stopped", - "net VMs stopped; guest routing, TAP connectivity, and dependent bridge traffic will be interrupted" - ], - "preserve": [ - "per-VM swtpm NVRAM and swtpm identity markers", - "declared host bridges, TAP naming intent, nftables/NM/networkd ownership metadata, and network-preflight evidence" - ], - "cutoverOnlyCleanup": [ - "/run/d2b-gpu", - "boot-scoped runtime socket files only after all d2b services are stopped" - ], - "failClosedHazards": [ - "symlink or path traversal inside any moved path", - "recursive operations traversing hardlink farms or mutating shared /nix/store inodes", - "any attempt to unlink lock files during cutover rather than leaving /run locks for reboot/tmpfs cleanup" - ], - "applyStatus": "not-implemented-in-this-build" -} -``` - -**Exit codes** - -| Code | Meaning | Typed error / reference | -| --- | --- | --- | -| `0` | Dry-run plan rendered. | - | -| `2` | Unknown flag or invalid flag combination. | [`usage`](./error-codes.md#usage) | -| `78` | `--apply` or `--rollback` requested before the broker-backed mover is available. | `storage-migration-apply-not-implemented`, `storage-migration-rollback-not-implemented` | - -**Human example** - -```text -$ d2b host migrate-storage --dry-run -host migrate-storage --dry-run: checkpoint=storage-cutover-… vm_count=2 -rollback command: d2b host migrate-storage --rollback --from-checkpoint storage-cutover-… -preflight requirements: - - all d2b VMs stopped - - d2bd.service stopped - - d2b-priv-broker.service stopped - - net VMs stopped; guest routing, TAP connectivity, and dependent bridge traffic will be interrupted -``` - -**Native** - -- `--dry-run` is a rust-native read-only planner. -- `--apply` and `--rollback` fail closed with typed exit-78 envelopes until the - broker-backed mover lands. There is no bash fallback and no manual - chmod/chown/setfacl remediation. - -**Bash** - -- No bash implementation exists. The Rust CLI owns this surface. - -The dry-run text deliberately avoids manual `chmod`/`chown`/`setfacl` -instructions. Operators should treat the checkpoint ID and rollback command as -the handoff contract for the later broker-backed cutover implementation. - ### `host reconcile-otel-acls` (reserved) **Synopsis:** `d2b host reconcile-otel-acls [--dry-run | --apply] [--human | --json]` @@ -2864,65 +2724,18 @@ Auth status is a read-only daemon query that reports caller mapping, socket reac ### `config sync` -**Synopsis:** `d2b activation config sync [--guest-path ] [--host ] [--user ] [--key ] [--known-hosts ] [--dry-run] [--json]` - - -On a guest-control-capable VM, `config sync` reads the VM's canonical -guest config working copy (default `/var/lib/d2b-guest/guest-config.nix`) -over the authenticated **guest-control transport** - a typed -`readGuestConfig` request to `d2bd` over the daemon public socket. -There is no SSH: no ssh/scp process is spawned, and the SSH-shaped flags -(`--host`/`--user`/`--key`/`--known-hosts`) and a non-default -`--guest-path` are rejected up front with `guest-control-ssh-flag-rejected` -(exit `2`). JSON responses carry `transport: "guest-control"`. - -The host treats the received bytes as untrusted data: the read is bounded -(1 MiB hard cap + a per-attempt timeout, so a hostile guest cannot -OOM/hang the host), the host re-enforces the size cap and recomputes size -+ sha256 from the received bytes (never a guest-reported value), the -content is validated (non-empty, valid UTF-8), and the result is -atomically written to a host-side **staging** file -(`${XDG_STATE_HOME:-~/.local/state}/d2b/config-staging/.guest.nix`). -The staging copy is never evaluated until approved. - -`--dry-run` selects and reports the transport WITHOUT contacting the -daemon or reading any guest bytes: it emits `transport: "guest-control"` -plus the planned staging target only - never an SSH argv and never guest -content. - -Fail-closed behaviour: - -- `config sync` is **admin-only**: `readGuestConfig` is gated to the - `d2b-admin` role (`d2b.site.adminUsers`) at the daemon's - `SO_PEERCRED` accept time. A launcher-role caller is rejected with the - typed `authz-not-admin` error (exit `75`, AUTH) before any socket - request reads guest bytes. The staging-only verbs (`diff`/`approve`/ - `reject`/`status`) dispatch no daemon verb and are not admin-gated. -- A known VM whose generation does not declare the guest-control transport - (old or partial generation) is rejected with - `guest-control-unavailable-old-generation` (exit `70`); no socket request - is sent and nothing is staged. The operator SSH-compatibility transport - is not wired into this command. -- When the daemon socket is unreachable, the command reports - `guest-control-transport-unavailable` (exit `70`). -- Per-kind read errors surfaced by the daemon - (`guest-control-file-not-found`, `guest-control-file-too-large`, - `guest-control-path-unsafe`, `guest-control-read-denied`, - `guest-control-timeout`, `guest-control-protocol-error`, - `guest-control-auth-failed`, `guest-control-capability-unavailable`) - each map to exit `70` with their slug and never echo guest content, - paths, or transport detail. - -`d2b host shutdown-hook --apply` is intentionally absent from the public -clap/completion surface and is invoked by `d2bd.service` only while the host -manager is stopping. When that hook connects as uid `0`, the daemon assigns the -narrow `HostShutdown` role. This role can dispatch only `vmStop`; it cannot run -admin-only operator verbs such as exec, USB attach/detach, host prepare/destroy, -audit export, key rotation, or config sync. - -**Disposition:** `rust-native` - host-initiated typed `readGuestConfig` -over the daemon public socket; no SSH, no virtiofs, no new privileged -surface. +**Synopsis:** `d2b activation config sync Guest/ [--dry-run] [--json]` + +Reads the canonical Guest configuration document through the authenticated +ConfigNixos service on the ComponentSession and writes a bounded, validated +copy to the host-side staging area. `--dry-run` reports the resource reference +and document identifier without contacting the target or reading guest bytes. +The operation is admin-only and fails closed when the target-local service, +ComponentSession, or document contract is unavailable. No SSH, raw file +transfer, or caller-supplied path is accepted. + +**Disposition:** `rust-native` - host-initiated typed ConfigNixos service call +over the daemon Resource API; no new privileged surface. ### `config diff` @@ -2974,14 +2787,15 @@ All `config` verbs share these exit codes: | --- | --- | | `0` | Success (including `diff` whether or not files differ). | | `1` | Runtime error: nothing staged, a low-level public-socket I/O failure on `config sync` (send/receive frame), size-cap/timeout on the staging verbs, missing `--to`/`--against` target dir, I/O error. | -| `2` | Usage error (bad/missing arguments; surfaced by `clap`), or `config sync` SSH-shaped flags rejected on a guest-control VM (`guest-control-ssh-flag-rejected`). | -| `70` | `config sync` only. The VM is not declared in the active manifest (`require_known_vm`); the VM's generation does not declare the guest-control transport (`guest-control-unavailable-old-generation`); the daemon socket is unreachable (`guest-control-transport-unavailable`); or a per-kind guest-control read error (`guest-control-file-not-found`, `guest-control-file-too-large`, `guest-control-path-unsafe`, `guest-control-read-denied`, `guest-control-timeout`, `guest-control-protocol-error`, `guest-control-auth-failed`, `guest-control-capability-unavailable`). The staging-only verbs (`diff`/`approve`/`reject`/`status`) do not consult the manifest or transport and so never return `70`. | +| `2` | Usage error (bad/missing arguments; surfaced by `clap`). | +| `70` | `config sync` only. The target or ConfigNixos service is unavailable, the ComponentSession cannot be established, or a typed document read/validation error occurs. The staging-only verbs (`diff`/`approve`/`reject`/`status`) do not consult the target service and so never return `70`. | | `75` | `config sync` only. The caller is not in `d2b.site.adminUsers`. `config sync` dispatches the admin-only `ReadGuestConfig` daemon verb, so a launcher-role peer is rejected with the typed `authz-not-admin` (AUTH) error - exit `75`, the daemon's reserved authz code - before any guest read. The staging-only verbs (`diff`/`approve`/`reject`/`status`) dispatch no daemon verb and so never return `75`. | With `--json` each verb emits a single stdout object: -- `config sync` → `{ "command": "config sync", "vm", "transport": "guest-control", "staging", "bytes", "sha256" }` - (or `{ "command": "config sync", "mode": "dry-run", "vm", "transport": "guest-control", "staging", "guestFile" }` under `--dry-run` - no SSH argv, no guest bytes). +- `config sync` → `{ "command": "config sync", "vm", "transport": "component-session", "staging", "bytes", "sha256" }` + (or `{ "command": "config sync", "mode": "dry-run", "resourceRef", "identifier" }` + under `--dry-run` - no target contact and no guest bytes). - `config diff` → `{ "command": "config diff", "vm", "against", "staging", "differs": , "diff": }`. - `config approve` → `{ "command": "config approve", "vm", "target", "bytes" }`. - `config reject` → `{ "command": "config reject", "vm", "removed": }`. @@ -3017,7 +2831,7 @@ List, status, detach, and kill are typed Resource requests. The retired public policy are resolved from the hash-verified private bundle and the exact requester-UID helper. Disconnect and named-stream close detach only; kill tears down only the verified shell scope. The host does not load realm credentials, -provider transports, raw guest-control frames, SSH, or provider-native shell +provider transports, raw component-session frames, SSH, or provider-native shell APIs. All shell actions remain admin-only. Launcher authorization for configured exec @@ -3140,7 +2954,7 @@ v3 Resource envelope; management commands use `EphemeralProcess/`. **Bash** - There is no live bash fallback for this verb; old-generation VMs fail - closed with a typed guest-control error. + closed with a typed component-session error. **Flags** @@ -3185,7 +2999,7 @@ the same login environment an interactive SSH login would (`XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, login-shell profile). `argv[0]` may be a bare program name or relative path; the workload user's login shell resolves it through that user's login `PATH` before `exec`. The -wire `user` field is host-fixed by `guestd` and ignored; operators +wire `user` field is host-fixed by `target-local Process` and ignored; operators elevate with `sudo` inside the session. The console replacement is: ```console @@ -3205,7 +3019,7 @@ Modes: `--json`. - **`-i`/`--interactive`**: forwards host stdin to the guest command (non-blocking, partial-write aware) until EOF, which closes the guest - stdin. The guest-control transport forwards stdin **only** in PTY + stdin. The component-session transport forwards stdin **only** in PTY mode, so `-i` **must** be paired with `-t` (use `-it`); `-i` without `-t` is a usage error. - **`-d`/`--detach`**: creates a non-interactive detached exec, prints @@ -3251,7 +3065,7 @@ Detached management: retained stdout/stderr bytes to the corresponding host streams and prints only bounded metadata warnings to stderr when bytes were dropped or truncated. An expired detached record is a typed failure - (`guest-control-exec-expired`, exit `76`), not a warning. JSON emits + (`component-session-exec-expired`, exit `76`), not a warning. JSON emits `{ "command": "vm exec logs", "vm": "", "execId": "", "stdoutBase64", "stderrBase64", "startOffset", "endOffset", "droppedBytes", "truncated" }`, plus per-stream @@ -3261,14 +3075,14 @@ Detached management: dropped and truncated accounting is metadata, not log content. - **kill** - `d2b exec kill EphemeralProcess/`: public name for - `ExecCancel`. Guestd requests graceful termination, waits a bounded + `ExecCancel`. target-local Process requests graceful termination, waits a bounded grace window, then force-kills the workload if needed. The operation is idempotent: human output confirms the result, and JSON emits `{ "command": "vm exec kill", "vm": "", "execId": "", "result": "cancelling"|"already-terminal", "state": "" }`. -Detached exec is supervised inside `guestd` and its in-guest detached -runner. It does not add a privileged broker operation. Guestd reconciles +Detached exec is supervised inside `target-local Process` and its in-guest detached +runner. It does not add a privileged broker operation. target-local Process reconciles detached runner/workload units before advertising detached capability, re-adopts structurally valid work, cleans orphaned workload units, and runs a periodic reaper for terminal records and retained-log slots. @@ -3280,12 +3094,12 @@ runs a periodic reaper for terminal records and retained-log slots. | `0` | cli | Detached create/list/status/logs/kill succeeded. | | `0`-`255` | guest | Attached guest command `WIFEXITED` status passes through unchanged. | | `128+N` | guest | An attached guest command was killed by signal `N` (`WIFSIGNALED`). | -| `2` | cli / guest-control | Usage error: missing command after `--`, unknown management verb without `--`, missing detached exec id, malformed `--env`, `-d` with `-i`/`-t`, `-t` without a terminal, `-i` without `-t`, `--json` combined with `-i`/`-t`, or guest-side `INVALID_PROGRAM` (`guest-control-invalid-program`) for an empty/leading-`-` program. | -| `69` | transport | The guest-control transport was unreachable, a per-op/establishment deadline elapsed, or `guestd` disappeared before the exec reported a terminal status (`guest-control-transport-unavailable`, `guest-control-timeout`, `guest-control-lost-guestd`). | -| `70` | guest-control | The VM generation does not support guest-control exec, or it lacks a required exec capability (`guest-control-unavailable-old-generation`, `guest-control-capability-unavailable`, `guest-control-exec-detached-unavailable`). No SSH fallback. | -| `75` | guest-control | The exec session table is at capacity, `Start` was rate limited, or an established session was cancelled/reaped before a terminal guest status arrived (`exec-session-capacity`, `exec-session-rate-limited`, `exec-session-cancelled`, `exec-session-reaped`). | -| `76` | protocol / guest-control | The guest returned a malformed/out-of-contract response, returned an op error, or no longer retains the requested detached record (`guest-control-protocol-error`, `guest-control-exec-error`, `guest-control-exec-not-found`, `guest-control-exec-expired`). | -| `77` | guest-control | The authenticated guest-control handshake was rejected (`guest-control-auth-failed`), the daemon's admin gate refused a non-admin caller (`authz-not-admin`), or a stale exec session was detected (`guest-control-stale-session`). | +| `2` | cli / component-session | Usage error: missing command after `--`, unknown management verb without `--`, missing detached exec id, malformed `--env`, `-d` with `-i`/`-t`, `-t` without a terminal, `-i` without `-t`, `--json` combined with `-i`/`-t`, or guest-side `INVALID_PROGRAM` (`component-session-invalid-program`) for an empty/leading-`-` program. | +| `69` | transport | The ComponentSession transport was unreachable, a per-op/establishment deadline elapsed, or the target-local Process disappeared before the exec reported a terminal status (`component-session-transport-unavailable`, `component-session-timeout`, `component-session-target-lost`). | +| `70` | component-session | The VM generation does not support component-session exec, or it lacks a required exec capability (`component-session-unavailable-old-generation`, `component-session-capability-unavailable`, `component-session-exec-detached-unavailable`). No SSH fallback. | +| `75` | component-session | The exec session table is at capacity, `Start` was rate limited, or an established session was cancelled/reaped before a terminal guest status arrived (`exec-session-capacity`, `exec-session-rate-limited`, `exec-session-cancelled`, `exec-session-reaped`). | +| `76` | protocol / component-session | The guest returned a malformed/out-of-contract response, returned an op error, or no longer retains the requested detached record (`component-session-protocol-error`, `component-session-exec-error`, `component-session-exec-not-found`, `component-session-exec-expired`). | +| `77` | component-session | The authenticated ComponentSession evidence was rejected (`component-session-auth-failed`), the daemon's admin gate refused a non-admin caller (`authz-not-admin`), or a stale exec session was detected (`component-session-stale-session`). | | `42` | internal | Daemon-internal or CLI-internal failure driving exec. | **Human example** @@ -3347,7 +3161,7 @@ shares the shell status number. single terminal stdout object. - success → `{ "command": "vm exec", "vm", "source": "guest", "exitCode", "reason": "exited"|"signaled", "guestExitCode"?|"signal"?, "stdoutBase64", "stderrBase64", "stdoutTruncated", "stderrTruncated" }`. Only a true guest `WIFEXITED`/`WIFSIGNALED` terminal is a success. -- failure → `{ "command": "vm exec", "vm", "source": "transport"|"guest-control"|"protocol"|"internal"|"cli", "reason": "", "exitCode", "transportExitCode"?, "message", "remediation"? }`. Abnormal terminal kinds (`lost-guestd`, `cancelled`, `reaped`) and a malformed/missing terminal status are failures with a reserved code and a non-`guest` source - never a synthesized guest exit. A failure envelope never carries captured stdio bytes. Usage errors (`source: "cli"`, exit `2`) also emit one envelope. +- failure → `{ "command": "vm exec", "vm", "source": "transport"|"component-session"|"protocol"|"internal"|"cli", "reason": "", "exitCode", "transportExitCode"?, "message", "remediation"? }`. Abnormal terminal kinds (`component-session-target-lost`, `cancelled`, `reaped`) and a malformed/missing terminal status are failures with a reserved code and a non-`guest` source - never a synthesized guest exit. A failure envelope never carries captured stdio bytes. Usage errors (`source: "cli"`, exit `2`) also emit one envelope. Captured output in JSON envelopes is bounded; `stdoutTruncated` / `stderrTruncated` flag a clamp. argv, env, cwd, and stdio bytes never @@ -3358,9 +3172,9 @@ Detached create/kill daemon audit is similarly redacted: VM, peer uid, closed action/result enums, and the opaque exec id only. **Disposition:** `rust-native` - daemon public socket → authenticated -guest-control session → `guestd` exec RPCs; no SSH, no host PTY, no new +ComponentSession session → target-local Process exec RPCs; no SSH, no host PTY, no new privileged broker op (attached sessions live in-process in `d2bd`; -detached state lives in guestd's detached registry). +detached state lives in target-local Process's detached registry). ## Dispatch capability table @@ -3372,9 +3186,9 @@ detached state lives in guestd's detached registry). | `guest start` | `rust-native` | Typed Guest lifecycle request; production UpdateSpec dispatch is a later-wave blocker. | | `guest stop` | `rust-native` | Typed Guest lifecycle request; production UpdateSpec dispatch is a later-wave blocker. | | `guest restart` | `rust-native` | Typed Guest lifecycle request; production UpdateSpec dispatch is a later-wave blocker. | -| `device usb attach` | `rust-native` | USBIP attach parses and dispatches one intent to `d2bd`; the daemon coordinates broker host bind/firewall/proxy state and authenticated guestd import over guest-control. | -| `device usb detach` | `rust-native` | USBIP detach parses and dispatches one intent to `d2bd`; the daemon asks guestd to detach matching imports, then runs broker `UsbipUnbind` / `UsbipProxyReconcile`. | -| `device usb probe` | `rust-native` | USBIP probe is a read-only daemon query backed by the broker's `UsbipProxyReconcile` validation pass. | +| `device usb attach` | `rust-native` | USBIP attach dispatches one typed Device Resource intent to `d2bd`; non-qemu requests fail closed until the signed target-local Process import effect path is available. | +| `device usb detach` | `rust-native` | USBIP detach dispatches one typed Device Resource intent to `d2bd`; non-qemu requests fail closed before Guest or host teardown until the signed target-local Process detach effect path is available. | +| `device usb probe` | `rust-native` | USBIP probe is a read-only daemon query that reports host/claim layers and Guest import unavailability without proxy mutation. | | `guest console` | `rust-native` | The Rust CLI owns help / argument validation and attaches to the daemon-native foreground console handoff via `ConsoleOp`, with provider-capability-aware streaming across Cloud Hypervisor, qemu-media, and ACA targets; see [provider capability matrix](./provider-capability-matrix.md). | | `audio status` | `rust-native` | The Rust CLI dispatches `AudioOp::Status` and renders provider-capability-aware per-target audio state/errors across Cloud Hypervisor, qemu-media, and ACA; see [provider capability matrix](./provider-capability-matrix.md). | | `audio mic` | `rust-native` | The Rust CLI dispatches microphone grant/revoke through `AudioOp::Mute`, persisting policy and applying host/guest enforcement according to provider capability. | @@ -3396,71 +3210,8 @@ detached state lives in guestd's detached registry). | `host prepare` | `rust-native` | The Rust CLI owns dry-run output (wired live); `--apply` is **not yet wired** - it returns the typed `daemon-down` envelope (exit `1`) today (use `--dry-run` for now). When the daemon-side dispatch ships, `--apply` will route through the daemon-backed `ApplyNftables` / `ApplyRoute` / `ApplySysctl` / `UpdateHostsFile` / `ApplyNmUnmanaged` sequence, with broker failures surfacing `broker-error` (exit `78`); a Tier-0 host is refused today (exit `78`). The historical bash fallback was retired in v1.0. | | `host destroy` | `rust-native` | The Rust CLI owns dry-run output (wired live); `--apply` is **not yet wired** - it returns the typed `daemon-down` envelope (exit `1`) today (use `--dry-run` for now). When the daemon-side dispatch ships, `--apply` will route through the reverse-order daemon-backed host-reconcile sequence, with broker failures surfacing `broker-error` (exit `78`); a Tier-0 host is refused today (exit `78`). The historical bash fallback was retired in v1.0. | | `host doctor` | `rust-native` | Host doctor is a read-only daemon health probe; `--read-only` is mandatory and there is no bash fallback for mutation forms. | -| `host migrate-storage` | `rust-native` | Storage cutover dry-run planning is native and read-only; `--apply` / `--rollback` fail closed until the broker-backed mover lands. | | `host install` | `rust-native` | Host install owns its dry-run preview in Rust and routes `--apply` through the daemon → broker `RunHostInstall` path without broker-error fallback to bash. | | `migrate` | `rust-native` | Dry-run analysis is native; `--apply` routes through `d2bd` → broker `RunMigrate`. Daemon-unreachable / native-handler-deferred conditions surface typed envelopes (exit `1` / exit `78` per ADR 0015); the historical bash fallback was retired in v1.0. | | `auth status` | `rust-native` | Auth status is a read-only daemon query that reports caller mapping, socket reachability, and authorization hints. | | `exec run/attach/wait/status/list/logs/kill` | `rust-native` | Typed EphemeralProcess Resource operations over the Zone session; no SSH or VM lifecycle alias. | -| `shell open/attach/list/status/detach/kill` | `rust-native` | Admin-only qualified ShellSession Resource lifecycle plus ProcessAttachClient named streams. Local VMs use authenticated guest-control; unsafe-local uses the exact requester-UID helper and a validated terminal fd. No retired `ShellOp`, SSH, host-shell fallback, root unit, per-VM service, or broker op. | - -### Host cutover and scoped reset - -The cutover surface is: - -```text -d2b host cutover preview [--system-artifact-id ] [--source-system-artifact-id ] -d2b host cutover status --operation-id -d2b host cutover hold --operation-id --reason -d2b host cutover resume --operation-id [--fresh-consent-digest ] -d2b host cutover apply --operation-id --candidate-id \ - --revision-plan-id --source-system-artifact-id \ - --preview-digest \ - --recovery-digest --operator-id --consent-digest \ - --handoff-file -d2b host cutover rollback --operation-id -d2b host cutover verify --operation-id -d2b host cutover doctor --operation-id -d2b host cutover finalize --operation-id --consent-file \ - --finalization-file -d2b host cutover reset --scope --target \ - --operation-id --candidate-id --revision-plan-id \ - --preview-digest --consent-digest --consent-file -``` - -`preview` is mutation-free and reports only redaction-safe inventory counts -and canonical digests. One-time cutover `preview`, `apply`, `verify`, and -`finalize` reject `--zone`; the operation is host-wide. Scoped reset is a -separate authority and requires its own scope, target, preview, and consent. -Reset admission binds the scope and target into a distinct operation capability; -it does not reuse the host-wide cutover capability. Destructive durable-Volume -reset additionally requires `--destroy-durable-volumes`, -`--destructive-consent-file`, and its matching digest; the default remains -Preserve. -`apply --handoff-file` supplies the existing typed host-generation handoff; -the CLI first obtains runner admission through `d2bd`, then submits the -handoff directly to the runner socket once the journal reaches the native -phase-4 disposition boundary; earlier phase skips are refused. If an admitted -runner is already present, a repeated apply with the handoff resumes through -that runner instead of requesting a duplicate launch; an admission-only retry -must supply the handoff file. -The candidate handoff's `systemArtifactId` and the preserved rollback -`sourceSystemArtifactId` are included in the admitted operation contract and -the preview/consent binding; a different artifact identity is refused before -any closure activation effect. Supply the same candidate and source artifact -identities to `preview` that are named by the corresponding typed handoffs. -If the apply response is lost after admission, the CLI emits the observed -runner state with `mutationAccepted=false` rather than converting an -possibly-mutating operation into a definitive command refusal. -`rollback --handoff-file` supplies the pre-apply generation handoff when the -native rollback boundary is at phase 4; the broker restores that typed -generation before the journal is terminally rolled back. -JSON evidence files are parsed through the canonical JSON profile and -normalized before the typed request crosses the Zone resource transport. -After admission, `status`, `hold`, `resume`, `rollback`, `verify`, `doctor`, and -`finalize` use the runner-owned Unix socket so they remain available while -`d2bd` is drained; `doctor` falls back to daemon observation before drain. -Verification refuses without authoritative post-activation -observations rather than inferring health from preview data. The journal is -root-owned mode `0600` and is never printed by the CLI. Hold and resume -advance only after the privileged audit boundary returns durable evidence; -otherwise they return a typed refusal without changing the journal. +| `shell open/attach/list/status/detach/kill` | `rust-native` | Admin-only qualified ShellSession Resource lifecycle plus ProcessAttachClient named streams. Local VMs use authenticated component-session; unsafe-local uses the exact requester-UID helper and a validated terminal fd. No retired `ShellOp`, SSH, host-shell fallback, root unit, per-VM service, or broker op. | diff --git a/docs/reference/cli-output/status.md b/docs/reference/cli-output/status.md index f246c9a075..0d4ecf14ab 100644 --- a/docs/reference/cli-output/status.md +++ b/docs/reference/cli-output/status.md @@ -22,7 +22,7 @@ form: it does **not** inline the bridge-health table. | `current` | string or `null` | Target of `/var/lib/d2b/vms//current`. | Stable wire contract. | | `booted` | string or `null` | Target of `/var/lib/d2b/vms//booted`. | Stable wire contract. | | `pendingRestart` | boolean | True when the VM is running and `booted != current`. | Stable wire contract. | -| `apiReady` | string, object, or `null` | Optional last daemon-observed Cloud Hypervisor API readiness state. Simple values are `yes`, `pending`, or `timeout`; the legacy object form is `{ "error": "" }`. Omitted, or legacy `null`, means no readiness result is known. Guest-control rollout must use a separate negotiated bounded status field rather than extending this free-form error string. | Stable wire contract. | +| `apiReady` | string, object, or `null` | Optional last daemon-observed Cloud Hypervisor API readiness state. Simple values are `yes`, `pending`, or `timeout`; the legacy object form is `{ "error": "" }`. Omitted, or legacy `null`, means no readiness result is known. ComponentSession readiness remains a separate typed Process role rather than an extension of this field. | Stable wire contract. | | `declaredRoles` | array of strings | Process-DAG roles declared for the VM in the trusted bundle. Video-enabled VMs include `video`; graphics VMs without `graphics.videoSidecar` omit it. | Stable wire contract. | | `readiness` | array of strings | Readiness predicates rendered as strings. Video-enabled VMs include `unix-socket-listening:/run/d2b-video//video.sock`; graphics VMs with video disabled omit video readiness because the video sidecar is a default-off capability. | Stable wire contract. | | `runtime` | string | Daemon runtime state label. | Stable wire contract. | @@ -44,10 +44,9 @@ form: it does **not** inline the bridge-health table. ## Stability promise The top-level keys and service-subkeys are frozen within this schema -revision. Guest-control rollout will add a negotiated guest-control status -field in the release that implements guest-control; until that schema revision lands, -operators discover old-generation guest-control state through the ADR 0028 -compatibility surfaces rather than an ad hoc extra key. +revision. ComponentSession readiness is represented by the typed +`component-session-health` Process role and typed incompatibility errors; it is +not encoded by extending `apiReady` or by adding an ad hoc status key. ## Human example diff --git a/docs/reference/cli-output/usb-probe.md b/docs/reference/cli-output/usb-probe.md index 905199e225..cc3b31d81e 100644 --- a/docs/reference/cli-output/usb-probe.md +++ b/docs/reference/cli-output/usb-probe.md @@ -5,10 +5,14 @@ Schema: [`usb-probe.schema.json`](./usb-probe.schema.json) `d2b usb probe --json` emits one object with `command = "usb probe"` and an `entries[]` array. The command is diagnostic: it asks `d2bd` to report declared USBIP session claims and qemu-media USB slots using redacted -physical-identity state labels. It may trigger bounded status -reconciliation (USBIP backend-ACL validation and qemu-media redacted -registry refresh), but it does not bind, unbind, import, detach, expose, -or release a USBIP device. +physical-identity state labels. It may refresh the independent qemu-media +registry, but it does not bind, unbind, import, detach, expose, reconcile the +USBIP proxy, or release a USBIP device. + +Until the signed target-local USBIP Process and authenticated ComponentSession +effect adapter are available, non-qemu attach, detach, and VM-start +reconciliation fail closed. A host claim alone is never a successful +attachment. ## Fields @@ -28,7 +32,7 @@ or release a USBIP device. | `entries[].host.bind` | string | Host driver bind state: `unbound`, `bound-to-usbip-host`, `bound-to-unexpected-driver`, `device-missing`, `unknown`, or `not-applicable`. | Stable additive field. | | `entries[].host.carrier` | string | Active carrier state for the `usbip-host` module, per-env backend/export readiness, and selected device presence: `absent`, `unavailable`, `withheld-for-owner`, `ready`, `departed-during-probe`, `unknown`, or `not-applicable`. | Stable additive field. | | `entries[].host.proxy` | string | Per-env proxy listener state: `not-declared`, `stopped`, `starting`, `listening`, `stale`, `failed`, `unknown`, or `not-applicable`. | Stable additive field. | -| `entries[].guest.import` | string | Guest import observation through authenticated guest-control: `detached`, `imported`, `unavailable`, `unknown`, or `not-applicable`. | Stable additive field. | +| `entries[].guest.import` | string | Guest import observation through authenticated component-session: `detached`, `imported`, `unavailable`, `unknown`, or `not-applicable`. | Stable additive field. | | `entries[].topologyPolicy.topology` | string | Redacted physical topology match state: `match`, `mismatch`, `incomplete`, `not-observed`, `not-applicable`, or `unknown`. | Stable additive field. | | `entries[].topologyPolicy.policy` | string | Bundle policy state: `allowed`, `denied`, `missing`, `not-applicable`, or `unknown`. | Stable additive field. | | `entries[].degradedReasons[]` | array | Closed degraded reason objects with `code`, `summary`, and `remediation`. Raw sysfs paths, serials, command output, and stderr are not included. | Stable additive field. | @@ -38,20 +42,19 @@ or release a USBIP device. ## Status and degradation rules - `bound` means the session claim is held by the desired VM and no degraded - reason was observed. -- `unbound` means the declared USBIP row has no session owner and can be - attached with `d2b usb attach --apply`. + reason was observed. Non-qemu rows with the current unavailable Guest import + path remain `degraded`. +- `unbound` means the declared USBIP row has no session owner. Non-qemu attach + still fails closed until the signed Guest Process path is available. - `degraded` means at least one session claim, active carrier, host bind, proxy, guest import, topology, or policy check did not converge. - A session lock alone is not healthy. A row whose lock is `held-by-desired-owner` remains `degraded` until host bind/carrier/proxy and guest import state converge. -- VM restart reconciliation preserves same-VM session claims. During restart - the daemon detaches guest imports and only runs host unbind after firewall - withdrawal plus targeted stream cleanup is proven, then replays - bind/proxy/import after guest-control readiness. Runtime absence or - guest/proxy unavailability is reported as degraded USB state; required - policy/topology failures fail before device exposure. +- VM restart reconciliation preserves same-VM session claims. Until Guest + detach/import is available, the daemon does not replay bind/proxy state or + release host state; Guest import unavailability is reported as degraded USB + state. Required policy/topology failures fail before device exposure. ## Degraded reasons @@ -67,9 +70,9 @@ or release a USBIP device. | `carrier-unavailable` | The `usbip-host` module, selected device, or per-env backend/export carrier is unavailable. | Reconnect the device and run `d2b usb attach --apply`. | | `host-bind-unavailable` | The device is not bound to `usbip-host`. | Run `d2b usb attach --apply` so the broker can bind it for export. | | `proxy-unavailable` | The per-env proxy is missing, stale, failed, or not listening. | Reconcile the proxy by running `d2b usb attach --apply`; inspect broker audit if it repeats. | -| `guest-import-unavailable` | The guest has not imported the claimed device or guest-control is unavailable. | Start the VM if needed, then run `d2b usb attach --apply`. | -| `stale-host-state` | Host export/proxy state remains after the claim was removed. | Run `d2b usb detach --apply` to drain host state. | -| `stale-guest-state` | Guest import state remains after the claim was removed. | Run `d2b usb detach --apply` so guestd removes the import. | +| `guest-import-unavailable` | The signed target-local USBIP Process or authenticated ComponentSession effect path is unavailable. | Use a build that includes that signed Process path, then retry the lifecycle command. | +| `stale-host-state` | Host export/proxy state remains after the claim was removed. | Do not mutate host state manually; preserve the claim until authenticated Guest detach and ordered host cleanup are available. | +| `stale-guest-state` | Guest import state remains after the claim was removed. | Do not mutate host state manually; preserve the claim until authenticated Guest detach is available. | | `probe-incomplete` | Probe did not produce a reconciliation-safe identity. | Retry `d2b usb probe`; if it repeats, verify the declaration has a stable physical selector. | ## JSON example @@ -95,7 +98,7 @@ or release a USBIP device. "proxy": "unknown" }, "guest": { - "import": "detached" + "import": "unavailable" }, "topologyPolicy": { "topology": "unknown", @@ -104,13 +107,11 @@ or release a USBIP device. "degradedReasons": [ { "code": "guest-import-unavailable", - "summary": "the guest USBIP import has not converged", - "remediation": "Run `d2b usb attach corp-vm 1-2 --apply` after the VM is running." + "summary": "The signed target-local USBIP Process or authenticated ComponentSession effect path is unavailable.", + "remediation": "Use a build that includes that signed Process path, then retry the lifecycle command." } ], - "remediationCommands": [ - "d2b usb attach corp-vm 1-2 --apply" - ] + "remediationCommands": [] } ] } diff --git a/docs/reference/cli-output/vm-audio-set.schema.json b/docs/reference/cli-output/vm-audio-set.schema.json index 85c0c4b856..44ddce92bf 100644 --- a/docs/reference/cli-output/vm-audio-set.schema.json +++ b/docs/reference/cli-output/vm-audio-set.schema.json @@ -76,14 +76,14 @@ "description": "Whether a set-volume or mute operation was applied and through which path.", "oneOf": [ { - "description": "Applied to both host (PipeWire/qemu) and guest (guestd).", + "description": "Applied to both host (PipeWire/qemu) and the target-local Process.", "type": "string", "enum": [ "host-and-guest" ] }, { - "description": "Applied to host only; guestd enforcement was unavailable or degraded.", + "description": "Applied to host only; target-local enforcement was unavailable or degraded.", "type": "string", "enum": [ "host-only" diff --git a/docs/reference/cli-output/vm-audio-status.schema.json b/docs/reference/cli-output/vm-audio-status.schema.json index d17bee23bc..f8c1a08812 100644 --- a/docs/reference/cli-output/vm-audio-status.schema.json +++ b/docs/reference/cli-output/vm-audio-status.schema.json @@ -33,21 +33,21 @@ "description": "How audio enforcement is applied for a target VM (ADR 0041).\n\nThis enum describes only *successful* enforcement outcomes. Provider misconfiguration is not a successful outcome and is reported exclusively through [`AudioVmError`] with `kind = AudioErrorKind::ProviderMisconfigured`.", "oneOf": [ { - "description": "Host-side PipeWire policy and guest-side guestd policy both applied.", + "description": "Host-side PipeWire policy and target-local Process policy both applied.", "type": "string", "enum": [ "host-and-guest" ] }, { - "description": "Host-side enforcement only; guestd is absent or the provider does not support guest enforcement.", + "description": "Host-side enforcement only; the provider does not support target-local enforcement.", "type": "string", "enum": [ "host-only" ] }, { - "description": "Guest-side guestd enforcement only; no local PipeWire node (e.g. ACA sandbox with a guestd-compatible agent).", + "description": "Target-local Process enforcement only; no local PipeWire node (for example, an ACA sandbox with a provider agent).", "type": "string", "enum": [ "guest-only" @@ -66,7 +66,7 @@ "description": "Low-cardinality error kind for a per-VM audio failure.\n\nUsed in [`AudioVmError`] and [`VmAudioErrorOutputV1`](crate::cli_output::VmAudioErrorOutputV1) so that both the wire protocol and the generated CLI schema carry enum constraints rather than a free-form string.\n\nSerde names match the canonical wire strings exactly.", "oneOf": [ { - "description": "The provider is expected to expose a guestd-compatible agent but none was found; operator remediation is required.", + "description": "The provider is expected to expose a target-local audio Process but none was found; operator remediation is required.", "type": "string", "enum": [ "provider-misconfigured" @@ -120,7 +120,7 @@ ] }, { - "description": "ACA sandbox with a guestd-compatible agent for audio policy.", + "description": "ACA sandbox with a provider agent for audio policy.", "type": "string", "enum": [ "aca-sandbox" diff --git a/docs/reference/cli-output/vm-exec-create.schema.json b/docs/reference/cli-output/vm-exec-create.schema.json index 04929b0a3b..9f94aea518 100644 --- a/docs/reference/cli-output/vm-exec-create.schema.json +++ b/docs/reference/cli-output/vm-exec-create.schema.json @@ -25,6 +25,7 @@ "additionalProperties": false, "definitions": { "ExecState": { + "description": "Lifecycle state projected for a target-local Process or EphemeralProcess resource.", "type": "string", "enum": [ "created", @@ -34,7 +35,7 @@ "cancelled", "slow-consumer-cancelled", "protocol-error", - "lost-guestd", + "lost-target", "reaped" ] } diff --git a/docs/reference/cli-output/vm-exec-kill.schema.json b/docs/reference/cli-output/vm-exec-kill.schema.json index c009e58917..8b23b7589d 100644 --- a/docs/reference/cli-output/vm-exec-kill.schema.json +++ b/docs/reference/cli-output/vm-exec-kill.schema.json @@ -37,6 +37,7 @@ ] }, "ExecState": { + "description": "Lifecycle state projected for a target-local Process or EphemeralProcess resource.", "type": "string", "enum": [ "created", @@ -46,7 +47,7 @@ "cancelled", "slow-consumer-cancelled", "protocol-error", - "lost-guestd", + "lost-target", "reaped" ] } diff --git a/docs/reference/cli-output/vm-exec-list.schema.json b/docs/reference/cli-output/vm-exec-list.schema.json index feaaf86522..65c547da3a 100644 --- a/docs/reference/cli-output/vm-exec-list.schema.json +++ b/docs/reference/cli-output/vm-exec-list.schema.json @@ -24,6 +24,7 @@ "additionalProperties": false, "definitions": { "ExecState": { + "description": "Lifecycle state projected for a target-local Process or EphemeralProcess resource.", "type": "string", "enum": [ "created", @@ -33,7 +34,7 @@ "cancelled", "slow-consumer-cancelled", "protocol-error", - "lost-guestd", + "lost-target", "reaped" ] }, diff --git a/docs/reference/cli-output/vm-exec-status.schema.json b/docs/reference/cli-output/vm-exec-status.schema.json index 0eb66964fc..05407f320a 100644 --- a/docs/reference/cli-output/vm-exec-status.schema.json +++ b/docs/reference/cli-output/vm-exec-status.schema.json @@ -68,6 +68,7 @@ "additionalProperties": false, "definitions": { "ExecState": { + "description": "Lifecycle state projected for a target-local Process or EphemeralProcess resource.", "type": "string", "enum": [ "created", @@ -77,7 +78,7 @@ "cancelled", "slow-consumer-cancelled", "protocol-error", - "lost-guestd", + "lost-target", "reaped" ] } diff --git a/docs/reference/companion-contracts.md b/docs/reference/companion-contracts.md index 979848afa7..efa897dbd8 100644 --- a/docs/reference/companion-contracts.md +++ b/docs/reference/companion-contracts.md @@ -31,7 +31,7 @@ holds the d2b 3.0 release. | Companion | Repository | Pinned commit | Maintainer of record | Discovery source | Exact consumed surface | Verification status | | --- | --- | --- | --- | --- | --- | --- | | `d2b-toolkit` | `github:vicondoa/d2b-toolkit` | **Unresolved:** no pinned companion commit is recorded; no network lookup was run. | **Unresolved:** no maintainer of record is stated in repository or contract records; no network lookup was run. | `docs/contributing/architecture.md`; `docs/how-to/configure-desktop-terminal-integration.md` | Client DTOs for the public daemon API; public-socket framing; Wayland color parsing; Waybar helpers. See [the v3 replacement contract](./zone-cli-contract.md) and [the UI color contract](./ui-colors.md). | **Pending live-host verification.** No release-candidate exercise is recorded by this publication. | -| `d2b-wlterm` | `github:vicondoa/d2b-wlterm` | **Unresolved:** no pinned companion commit is recorded; no network lookup was run. | **Unresolved:** no maintainer of record is stated in repository or contract records; no network lookup was run. | `docs/contributing/architecture.md`; `docs/how-to/configure-desktop-terminal-integration.md` | Qualified ShellSession Resource lifecycle, ProcessAttachClient named streams, provider-neutral launcher metadata, canonical Host/Guest execution references, and the `d2b-wayland-proxy` package. The retired public-socket `ShellOp` family is not supported. See [the persistent-shell reference](./guest-control-persistent-shell.md), [the v3 replacement contract](./zone-cli-contract.md), and [the desktop integration guide](../how-to/configure-desktop-terminal-integration.md). | **Pending live-host verification.** No release-candidate exercise is recorded by this publication. | +| `d2b-wlterm` | `github:vicondoa/d2b-wlterm` | **Unresolved:** no pinned companion commit is recorded; no network lookup was run. | **Unresolved:** no maintainer of record is stated in repository or contract records; no network lookup was run. | `docs/contributing/architecture.md`; `docs/how-to/configure-desktop-terminal-integration.md` | Qualified ShellSession Resource lifecycle, ProcessAttachClient named streams, provider-neutral launcher metadata, canonical Host/Guest execution references, and the `d2b-wayland-proxy` package. The retired public-socket `ShellOp` family is not supported. See [the persistent-shell reference](../explanation/persistent-shells.md), [the v3 replacement contract](./zone-cli-contract.md), and [the desktop integration guide](../how-to/configure-desktop-terminal-integration.md). | **Pending live-host verification.** No release-candidate exercise is recorded by this publication. | | `d2b-wlcontrol` | **Unresolved:** no repository URL is recorded in repository or contract records. | **Unresolved:** no pinned companion commit is recorded; no network lookup was run. | **Unresolved:** no maintainer of record is stated in repository or contract records; no network lookup was run. | `docs/adr/0040-graceful-vm-shutdown.md`; `docs/adr/0041-console-and-audio-controls.md`; `docs/how-to/use-console-and-audio.md` | The public daemon socket; `/etc/d2b/ui-colors.json`; `/etc/d2b/ui-colors.css`; `d2b audio status --json`; security-key state and action DTOs (`WlcontrolSkStatus`, `WlcontrolCeremonyRow`, and `WlcontrolAction`); the `d2b device security-key status`, `sessions`, and `cancel` operations; and graceful-stop semantics, including the distinction between normal stop and `--force`. See [the UI color contract](./ui-colors.md), [the CLI contract](./cli-contract.md), and [the v3 replacement contract](./zone-cli-contract.md). | **Pending live-host verification.** No release-candidate exercise is recorded by this publication. | | `d2b-clip-picker` | `github:vicondoa/d2b-clip-picker` | **Unresolved:** no pinned companion commit is recorded; no network lookup was run. | **Unresolved:** no maintainer of record is stated in repository or contract records; no network lookup was run. | `docs/adr/0042-d2b-clipboard-authority-and-picker-split.md`; `docs/how-to/configure-clipboard-picker.md` | The versioned newline-delimited JSON picker protocol over an inherited anonymous Unix `socketpair()` file descriptor; `ClientHello`, `OpenRequest`, `Select`, and `Cancel`; canonical realm/workload target names; and resolved accent colors. See [the picker protocol](./clipboard-picker-protocol.md) and [realm-core](./realm-core.md). | **Pending live-host verification.** No release-candidate exercise is recorded by this publication. | diff --git a/docs/reference/components-audio.md b/docs/reference/components-audio.md index edfaf5a6c0..058924b4b8 100644 --- a/docs/reference/components-audio.md +++ b/docs/reference/components-audio.md @@ -24,9 +24,9 @@ Console and audio are provider-capability-aware daemon surfaces; the per-provider capability matrix and enforcement model are documented in [provider capability matrix](./provider-capability-matrix.md) and [ADR 0041](../adr/0041-console-and-audio-controls.md). The matrix covers -Cloud Hypervisor NixOS VMs (vhost-user-sound + guestd enforcement), +Cloud Hypervisor NixOS VMs (vhost-user-sound + target-local Process enforcement), qemu-media targets (host/qemu subset only; guest enforcement unsupported), -and ACA sandboxes (remote guestd policy only; no local host mutations). +and ACA sandboxes (remote provider-agent policy only; no local host mutations). Each VM has independent **mic** and **speaker** grants. The host-side state file is `/var/lib/d2b/vms//state/audio-state.json`. diff --git a/docs/reference/components-graphics.md b/docs/reference/components-graphics.md index 63c983a4e0..31fd4ea94d 100644 --- a/docs/reference/components-graphics.md +++ b/docs/reference/components-graphics.md @@ -23,7 +23,7 @@ When `graphics.crossDomainTrusted = true` and the guest socket, while the host-side `d2b-wayland-proxy` runs as a broker-spawned `wayland-proxy` role and mediates access to the real host compositor. `d2bd` supervises the daemon-owned process DAG and asks -`d2b-priv-broker` to spawn the wayland proxy, GPU sidecar +`d2b-broker` to spawn the wayland proxy, GPU sidecar (`crosvm device gpu`), and cloud-hypervisor runner as pidfd-tracked runners. The GPU sidecar runs as the dedicated per-VM `d2b--gpu` system user, not as the operator's Wayland user. @@ -62,26 +62,14 @@ protocol features remain disabled until the proxy can validate seat-bound requests safely, avoiding guest application crashes from invalid forwarded text-input requests under Niri-backed cross-domain Wayland. -## Host-app terminal proxy mode +## Host-app terminal integration The flake exports `packages..d2b-wayland-proxy` for host tools such as `d2b-wlterm` that must resolve the supported proxy binary without relying on an -internal package path or the operator's `PATH`. The binary also has a -foreground host-terminal launch path: - -```bash -d2b-wayland-proxy --host-terminal --vm-name work --border-enable -- wezterm start -``` - -In this mode the proxy derives the upstream compositor from `--connect` or -`$WAYLAND_DISPLAY`, creates a randomized single-use listen socket below -`$XDG_RUNTIME_DIR/d2b-wayland-proxy//`, forces that directory to `0700`, -removes only stale socket files at the selected paths, and chmods the listen -socket to `0600` before launching the child. `WAYLAND_DISPLAY` is set to the -single-use proxy socket and `WEZTERM_UNIX_SOCKET` is set to a randomized -per-VM mux socket, so the terminal does not reuse the operator's global WezTerm -daemon. The proxy opens a close-on-exec pidfd for the child, waits in the -foreground, and removes the single-use socket paths when the process exits. +internal package path or the operator's `PATH`. The proxy accepts only an +explicit upstream compositor and listen socket; desktop terminal processes +are launched by their owning signed Process or companion, never by the +Wayland proxy. The same Wayland security policy applies to the terminal child: ordinary application globals needed by WezTerm remain available, privileged globals such @@ -152,7 +140,7 @@ The matching guest-visible option lives in the imported allowlist). The `.bpf` files live alongside the crosvm binary under a `symlinkJoin`; the C parser fallback is never used. - **`wl-cross-domain-proxy`** packaged under `pkgs/` for the guest-side - virtio-gpu cross-domain bridge. + virtio-gpu cross-domain bridge and retained in trusted guest closures. ## Guest-side resources created @@ -164,9 +152,15 @@ The matching guest-visible option lives in the imported - `microvm.graphics.crosvmPackage` = either `crosvmPatched` (cross-domain trusted) or a shell shim around `crosvmPatched` that strips `cross-domain` from `--params`. -- `systemd.user.services.wayland-proxy` - when - `crossDomainTrusted = true`, runs `wl-cross-domain-proxy` for the - guest-side virtio-gpu cross-domain bridge. +- The signed `wayland-frontend-worker` Guest Process child - when + `crossDomainTrusted = true`, runs `wl-cross-domain-proxy` through the + `WaylandSession` lifecycle; no direct `systemd.user.services` launch is + emitted. +- The Host Process controller records the Guest frontend as durable intent but + does not launch or mark Guest execution Ready. A target-local Guest Process + controller must reconcile that child through the authenticated Guest + boundary; until then the display aggregate remains pending and no Host + service fallback is used. - `environment.sessionVariables` pinning `WAYLAND_DISPLAY`, `QT_QPA_PLATFORM`, `GDK_BACKEND`, `XDG_SESSION_TYPE`, `SDL_VIDEODRIVER`, `CLUTTER_BACKEND`, `MOZ_ENABLE_WAYLAND`, plus diff --git a/docs/reference/components-shell.md b/docs/reference/components-shell.md index 4da5d8394a..34ab48e48f 100644 --- a/docs/reference/components-shell.md +++ b/docs/reference/components-shell.md @@ -26,20 +26,17 @@ d2b.vms..guest.shell = { Enabling `guest.shell` requires: -- `guest.control.enable = true`; -- `guest.exec.enable = true`; -- a non-root workload user (`ssh.user`); -- a runtime/provider that supports guest-control shell operation capability. +- `guest.componentSession.enable = true`; +- a runtime/provider that supports the signed target-local shell Process. -`qemu-media` and providers without guest-control reject non-default +`qemu-media` and providers without component-session reject non-default `guest.shell.*` settings at eval time. ## Guest wiring When enabled for a workload user, the guest module: -- passes `--shell-enable`, `--shell-default-name`, `--shell-max-sessions`, and - `--shell-max-attached` to guestd; +- passes the shell policy to the signed target-local Process; - wires the static `d2b-guest-shell-runner` and `systemctl` paths; - declares `d2b-shpool-daemon.service` as the workload user with `PAMName=d2b-shpool-daemon`; @@ -64,7 +61,7 @@ Supported providers emit a per-VM manifest block: } ``` -Providers without guest-control emit `shell = null`. The manifest never exposes +Providers without component-session emit `shell = null`. The manifest never exposes runtime helper sockets, terminal handles, shpool state paths, or live session names beyond the configured default. diff --git a/docs/reference/components-usb-security-key.md b/docs/reference/components-usb-security-key.md index a346a0fbe7..b613cf36f8 100644 --- a/docs/reference/components-usb-security-key.md +++ b/docs/reference/components-usb-security-key.md @@ -68,9 +68,9 @@ The following conditions are enforced at NixOS eval time (before any build): cannot both be set for the same VM. The assertion message names the conflicting VM. -3. **Guest control prerequisite**: `usb.securityKey.enable = true` requires - `guest.control.enable = true` on the same VM (the DAG supervisor needs - guest control to manage the frontend node lifecycle). +3. **ComponentSession prerequisite**: `usb.securityKey.enable = true` requires + `guest.componentSession.enable = true` on the same VM (the DAG supervisor needs + ComponentSession to manage the frontend node lifecycle). ## Device selectors @@ -96,7 +96,7 @@ stable label, USB vendor/product IDs, and an optional serial: The host broker owns the following runtime resources. They are **not** declared as NixOS systemd services or static units; they are created and torn down by -the `d2bd`/`d2b-priv-broker` pipeline at runtime: +the `d2bd`/`d2b-broker` pipeline at runtime: | Resource | Owner | Path / name | |----------|-------|-------------| diff --git a/docs/reference/components-usbip.md b/docs/reference/components-usbip.md index 9c38c9b45a..e47a2feda1 100644 --- a/docs/reference/components-usbip.md +++ b/docs/reference/components-usbip.md @@ -7,22 +7,19 @@ ## What this component does -Enables on-demand passthrough of a host-side YubiKey (USB vendor ID -`1050`) into a VM via USBIP. When `d2b.site.yubikey.enable = true` -and some enabled VM in an env sets `usbip.yubikey = true`, the host -materializes a broker-spawned per-env `usbipd` backend listening on TCP -`` (usbipd has no `--host` flag, so it binds to -`0.0.0.0`; firewall rules - see "Host-side resources" - restrict -backend ingress to host loopback, so it's the operational equivalent -of a loopback bind but enforced via netfilter rather than by the -socket). A broker-spawned per-env `socat` proxy binds exactly the env's -uplink-bridge IP at TCP 3240; the guest loads `vhci_hcd`, ships the -`usbip` CLI, and advertises guestd's `UsbipImport` capability so -`d2bd` can import/detach through authenticated guest-control. The -hot-plug ceremony is daemon-owned: host bind/unbind and firewall/proxy -reconcile go through the privileged broker, while guest attach/detach goes -through guestd. The CLI sends one intent to `d2bd`; it never SSHes into -the guest for USBIP. +Enables the guest-side kernel and package prerequisites for on-demand +passthrough of a host-side YubiKey (USB vendor ID `1050`) into a VM via USBIP. +When `d2b.site.yubikey.enable = true` and some enabled VM in an env sets +`usbip.yubikey = true`, the host materializes a broker-spawned per-env +`usbipd` backend and proxy as described below. The guest loads `vhci_hcd` and +ships the `usbip` CLI. + +The signed target-local USBIP Process and its authenticated ComponentSession +effect adapter are not currently wired into the daemon. Until that contract +exists, non-qemu USBIP attach, detach, and VM-start reconciliation fail closed +with `runtime-capability-unsupported`; no host bind, proxy reconciliation, or +claim release is reported as a successful guest attachment. The CLI never +SSHes into the guest or uses guestd. The component itself only declares the **guest-side** wiring. All host-side machinery (usbipd backend + proxy broker-spawned runners, @@ -45,7 +42,7 @@ owners and remediation: `d2b device usb detach --apply` releases a healthy claim during that host session. - **Active carrier** - transient host/guest state that can disappear across - unplug, VM stop, daemon restart, or guest-control restart: the + unplug, VM stop, daemon restart, or component-session restart: the `usbip-host` module, host driver bind, per-env backend/export readiness, per-env proxy listener, and guest import. - **Policy/topology** - bundle-declared vendor/product and bus/port @@ -53,14 +50,13 @@ owners and remediation: fixing the declaration or attaching the approved physical device, then rebuilding before retry. -VM stop/restart cleans up guest imports and only runs host unbind when firewall -withdrawal plus targeted stream cleanup can be proven first; otherwise it keeps -the same-VM session claim for manual recovery. VM start reconciles same-VM -session claims from the current host session after guest-control readiness by -replaying host bind/proxy state and re-importing in the guest. Runtime absence, -proxy/backend unavailability, or guest import unavailability degrades -`d2b device usb probe` / `d2b guest status ` without pretending the row is healthy. -Required policy/topology failures remain fail-closed. +VM stop/restart must drain the guest import before host release. Because the +target-local USBIP Process is currently unavailable, stop cleanup preserves the +same-VM session claim and does not unbind or release host state. VM start does +not replay host bind/proxy state while the Guest status path is unavailable. +`d2b device usb probe` reports guest import as unavailable and keeps affected +claims degraded rather than pretending the row is healthy. Required +policy/topology failures remain fail-closed. ## Options (host-side) @@ -94,7 +90,7 @@ Per opted-in env (declared in [`network.nix`](../../packages/d2b-provider-networ > `ModprobeIfAllowed{module: "usbip-host"}` runs before the first > `UsbipBackend` runner for each env. Per-attach host `usbip bind` / > `unbind` steps are broker ops with per-env busid locking and audit -> coverage; guest `usbip attach` / `detach` is an authenticated guestd RPC. +> coverage; guest `usbip attach` / `detach` is an authenticated target-local Process RPC. > VM start/stop USB reconciliation threads one bounded reconcile correlation ID > through its USB broker requests as the broker audit `tracingSpanId`. > These privileged USB broker requests inherit the broker IPC limiter @@ -198,10 +194,11 @@ Per host (in [`host.nix`](../../nixos-modules/host.nix)): ## Runtime prerequisite contract -For `d2b device usb attach --apply` to expose a device, all of -these must be true: +For a future implementation, +`d2b device usb attach --apply` may expose a device only when +all of these are true: -1. the target VM is running and guest-control advertises USBIP status/import; +1. the target VM is running and component-session advertises USBIP status/import; 2. the bundle declares USBIP bind/firewall intents for the VM and busid; 3. the session busid claim is missing or already held by the target VM; 4. `usbip-host`, the physical device, host bind operation, per-env backend, @@ -214,24 +211,24 @@ Stable operator remediation uses lifecycle verbs rather than direct lock or sysfs mutation. Keep procedural recovery in the how-to runbook: [Troubleshoot USBIP passthrough](../how-to/troubleshoot-usbip.md). -CLI contract (`d2b device usb attach|detach|probe` in the Rust CLI): +Current CLI behavior (`d2b device usb attach|detach|probe` in the Rust CLI): - Sends one apply/dry-run intent to `d2bd`. -- `attach --apply`: guestd first detaches any stale matching import, the - broker binds/locks the host busid and reconciles firewall/proxy state, then - guestd imports the device inside the VM. -- `detach --apply`: for the generic per-env L4 proxy, the daemon first requires - an immediate-revocation proof: firewall block/withdrawal must precede any - targeted conntrack deletion or TCP established-socket kill for a proven - VM/proxy tuple whose source is not hidden by SNAT and whose anti-spoofing - posture is proven. When that proof is unavailable, detach returns the public - revocation-isolation failure with the target busid and preserves the - broker-owned claim for manual drain/recovery instead of silently leaving - an established stream or bouncing unrelated same-env streams. +- `attach --apply` and `detach --apply`: non-qemu requests return typed + `runtime-capability-unsupported` until the target-local Process and + authenticated Guest import/detach effect path is available. They do not + bind, unbind, reconcile the host proxy, or release the claim. +- `probe`: reports the observed host and claim layers, marks Guest import + unavailable, and does not mutate the host proxy. + +When the Guest effect path is implemented, detach must first prove firewall +withdrawal and targeted stream cleanup for the generic per-env L4 proxy before +releasing host state. Ambiguous or shared streams must continue to fail closed. ### Proxy synchronization strategy -The current proxy is per-env, not per-busid: a `socat` L4 listener forwards +When the Guest effect path is available, the proxy is per-env, not per-busid: +a `socat` L4 listener forwards `:3240` to that env's loopback backend port. Synchronization therefore follows the conservative daemon plan in `packages/d2b-provider-device-usbip/src/reconcile_state.rs`: @@ -255,6 +252,11 @@ therefore follows the conservative daemon plan in This means a single VM restart in an env must not disconnect unrelated active USBIP streams in that same env. +The current U10 daemon does not execute this host-side synchronization for +non-qemu USBIP requests: attach and start stop before host bind/proxy effects, +probe is observational, and stop/detach preserve the claim until Guest detach +can be authenticated. + ## Guest-side resources created The entire `components/usbip.nix` is two lines of payload: @@ -263,16 +265,16 @@ The entire `components/usbip.nix` is two lines of payload: { boot.kernelModules = [ "vhci_hcd" ]; environment.systemPackages = [ pkgs.linuxPackages.usbip ]; - d2b.guestControl.usbipPath = ".../bin/usbip"; } ``` - `vhci_hcd` lets `usbip attach` materialise the redirected device as `/dev/hidraw` (or a raw USB node) inside the guest kernel. -- The `usbip` CLI is needed in-guest so guestd can issue `usbip port`, - `usbip detach`, and `usbip attach` after authenticating the host over - guest-control. Host-side `usbip bind/unbind`, firewall, and proxy - reconciliation dispatch through the daemon → broker path. +- The `usbip` CLI is reserved for the future signed target-local Process to + issue `usbip port`, `usbip detach`, and `usbip attach` after authenticating + the host over ComponentSession. No current daemon path invokes those guest + commands. Host-side `usbip bind/unbind`, firewall, and proxy reconciliation + remain broker-owned and are withheld when Guest import is unavailable. ## Runtime invariants @@ -285,7 +287,7 @@ The entire `components/usbip.nix` is two lines of payload: the nftables carve-out (per ADR 0013 + this doc's "Firewall carve-outs" section above) keys on the env's own uplink bridge, host destination IP, and net-VM uplink source IP. -- Guestd's `usbip attach` connects to its own env's +- target-local Process's `usbip attach` connects to its own env's `usbipdHostIp` (the host-side end of that env's uplink bridge), not the host's WAN address. diff --git a/docs/reference/components-video.md b/docs/reference/components-video.md index 84bea8a2e8..3c21b7b188 100644 --- a/docs/reference/components-video.md +++ b/docs/reference/components-video.md @@ -54,7 +54,7 @@ Video decode is distinct from display streaming and GPU acceleration; see ## Host-side resources When enabled, the daemon-owned process DAG adds a `video` node. There is no -per-VM systemd unit; `d2bd` asks `d2b-priv-broker` to spawn the runner +per-VM systemd unit; `d2bd` asks `d2b-broker` to spawn the runner with `SpawnRunner { role: Video }` and tracks it by pidfd. | Resource | Shape | diff --git a/docs/reference/daemon-api.md b/docs/reference/daemon-api.md index 2fab547cd5..9a753afedc 100644 --- a/docs/reference/daemon-api.md +++ b/docs/reference/daemon-api.md @@ -79,16 +79,13 @@ compatibility surface. | `CapabilityNegotiation` | struct | [`CapabilityNegotiation`](../../packages/d2b-contracts/src/capability.rs#L328) | struct { `schema_version`: `u32`; `capabilities`: `CapabilitySet`; `fingerprint`: `String` } | | `CapabilitySet` | struct | [`CapabilitySet`](../../packages/d2b-contracts/src/capability.rs#L143) | struct { `known`: `BTreeSet`; `unknown`: `BTreeSet` } | | `FeatureFlag` | struct | [`FeatureFlag`](../../packages/d2b-contracts/src/lib.rs#L50) | empty struct | -| `GuestCapability` | enum | [`GuestCapability`](../../packages/d2b-contracts-control/src/guest_wire.rs#L351) | `Health`; `Capabilities`; `ExecAttached`; `ExecDetached`; `ExecTty`; `ExecLogs`; `TtyResize`; `Signals`; `ReadGuestFile`; `UsbipImport`; `ShellAttached`; `ShellManagement`; `ShellForceAttach`; `UsbipStatus`; `SystemActivation`; `AudioStatus`; `AudioSet` | -| `Hello` | struct | [`Hello`](../../packages/d2b-contracts/src/lib.rs#L134) | struct { `client_version`: `SemverRange`; `supported_features`: `Vec` } | -| `HelloOk` | struct | [`HelloOk`](../../packages/d2b-contracts/src/lib.rs#L142) | struct { `server_version`: `Version`; `selected_version`: `Version`; `capabilities`: `Vec` } | -| `HelloRejected` | struct | [`HelloRejected`](../../packages/d2b-contracts/src/lib.rs#L150) | struct { `reason`: `HelloRejectedReason` } | -| `HelloRejectedReason` | enum | [`HelloRejectedReason`](../../packages/d2b-contracts/src/lib.rs#L156) | `VersionMismatch`; `CapabilityNegotiationFailed`; `InternalError` | -| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1443) | struct { `client_version`: `String`; `supported_features`: `Vec` } | -| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-control/src/guest_wire.rs#L581) | struct { `metadata`: `GuestRequestMetadata`; `host_nonce`: `GuestNonce`; `transcript_version`: `u32` } | -| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1582) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | -| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-control/src/guest_wire.rs#L589) | struct { `guest_nonce`: `GuestNonce`; `guest_boot_id`: `GuestBootId`; `protocol_version`: `u32` } | -| `KnownFeatureFlag` | enum | [`KnownFeatureFlag`](../../packages/d2b-contracts/src/lib.rs#L104) | `TypedErrors`; `ManifestV04`; `StatusCheckBridges`; `ExportBrokerAudit`; `ConfiguredLaunchV1`; `UnsafeLocalProviderV1`; `CutoverRunnerV1` | +| `Hello` | struct | [`Hello`](../../packages/d2b-contracts/src/lib.rs#L131) | struct { `client_version`: `SemverRange`; `supported_features`: `Vec` } | +| `HelloOk` | struct | [`HelloOk`](../../packages/d2b-contracts/src/lib.rs#L139) | struct { `server_version`: `Version`; `selected_version`: `Version`; `capabilities`: `Vec` } | +| `HelloRejected` | struct | [`HelloRejected`](../../packages/d2b-contracts/src/lib.rs#L147) | struct { `reason`: `HelloRejectedReason` } | +| `HelloRejectedReason` | enum | [`HelloRejectedReason`](../../packages/d2b-contracts/src/lib.rs#L153) | `VersionMismatch`; `CapabilityNegotiationFailed`; `InternalError` | +| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1381) | struct { `client_version`: `String`; `supported_features`: `Vec` } | +| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1513) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | +| `KnownFeatureFlag` | enum | [`KnownFeatureFlag`](../../packages/d2b-contracts/src/lib.rs#L103) | `TypedErrors`; `ManifestV04`; `StatusCheckBridges`; `ExportBrokerAudit`; `ConfiguredLaunchV1`; `UnsafeLocalProviderV1` | | `SemverRange` | struct | [`SemverRange`](../../packages/d2b-contracts/src/error.rs#L1135) | empty struct | @@ -100,8 +97,9 @@ The feature-negotiated `workload` frame carries a `WorkloadOp` body and returns `workloadResponse`. `List` and `Status` expose provider-neutral, argv-free metadata. `LauncherExec` accepts only canonical target, item id, and operation id. The daemon reloads and hash-verifies the trusted bundle, cross-checks public -item metadata against its private configured item, and then routes to -guest-control or the exact requester-UID unsafe-local helper. Remote/relay +item metadata against its private configured item, and then routes to the +target-local Process owner or the exact requester-UID unsafe-local helper. +Remote/relay principals, UID fields, argv, environment, cwd, proxy paths, process ids, unit names, and fallback transports are not accepted. @@ -126,9 +124,9 @@ multiplexes terminal-v1 operations behind the named stream. Public requests carry no policy, uid, argv, environment, cwd, path, or provider override. Local-VM launcher execution uses a deterministic opaque guest exec id derived -from the local peer uid and public operation identity. Guestd's durable detached -record is the restart-stable idempotency authority; the same id and argv hash -replay the existing exec, while a hash mismatch is rejected. +from the local peer uid and public operation identity. The target-local +EphemeralProcess record is the restart-stable idempotency authority; the same +id and argv hash replay the existing exec, while a hash mismatch is rejected. Daemon audit records the authenticated `peer_uid` and public `operation_id` on each configured-launch outcome. Local-VM events also carry the opaque guest @@ -252,115 +250,108 @@ host reboot. | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `PublicRequest` | enum | [`PublicRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L29) | `Capabilities`; `AuthStatus`; `List` - (ListRequest); `Status` - (StatusRequest); `Audit` - (AuditRequest); `HostCheck` - (HostCheckRequest); `VmStart` - (VmLifecycleRequest); `VmStop` - (VmLifecycleRequest); `VmRestart` - (VmLifecycleRequest); `Switch` - (ActivationRequest); `Boot` - (ActivationRequest); `Test` - (ActivationRequest); `Rollback` - (ActivationRequest); `Gc` - (GcRequest); `KeysList`; `KeysShow` - (KeysShowRequest); `KeysRotate` - (KeysRotateRequest); `Trust` - (TrustRequest); `RotateKnownHost` - (RotateKnownHostRequest); `UsbipBind` - (UsbipBindCliRequest); `UsbipUnbind` - (UsbipUnbindCliRequest); `UsbipProbe`; `StoreVerify` - (StoreVerifyRequest); `Migrate` - (MigrateRequest); `HostPrepare` - (HostPrepareRequest); `HostDestroy` - (HostDestroyRequest); `HostInstall` - (HostInstallRequest); `HostReconcile` - (HostReconcileRequest); `HostCutover` - (HostCutoverRequest); `ReadGuestConfig` - (ReadGuestConfigRequest); `Exec` - (ExecOp); `Console` - (ConsoleOp); `Audio` - (AudioOp); `GatewayDisplay` - (GatewayDisplayOp); `Workload` - (WorkloadOp); `UsbSecurityKeyStatus`; `UsbSecurityKeySessions`; `UsbSecurityKeyCancel` - (d2b_contracts::security_key::SecurityKeyCancelRequest) | -| `ListRequest` | struct | [`ListRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L438) | struct { `env`: `Option`; `vm`: `Option` } | -| `StatusRequest` | struct | [`StatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L445) | struct { `check_bridges`: `bool`; `vm`: `Option` } | -| `AuditRequest` | struct | [`AuditRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L453) | struct { `filter`: `Option`; `format`: `AuditFormat`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | -| `HostCheckRequest` | struct | [`HostCheckRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L470) | struct { `read_only`: `bool`; `strict`: `bool` } | -| `VmLifecycleRequest` | struct | [`VmLifecycleRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L496) | struct { `vm`: `String`; `flags`: `MutationFlags`; `force`: `bool`; `no_wait_api`: `bool` } | -| `ActivationRequest` | struct | [`ActivationRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L516) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `GcRequest` | struct | [`GcRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L524) | struct { `flags`: `MutationFlags`; `keep_generations`: `Option` } | -| `KeysShowRequest` | struct | [`KeysShowRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L533) | struct { `vm`: `String` } | -| `KeysRotateRequest` | struct | [`KeysRotateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L539) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `TrustRequest` | struct | [`TrustRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L547) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `RotateKnownHostRequest` | struct | [`RotateKnownHostRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L555) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `UsbipBindCliRequest` | struct | [`UsbipBindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L563) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | -| `UsbipUnbindCliRequest` | struct | [`UsbipUnbindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L572) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | -| `StoreVerifyRequest` | struct | [`StoreVerifyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L581) | struct { `vm`: `String`; `repair`: `bool` } | -| `ReadGuestConfigRequest` | struct | [`ReadGuestConfigRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L594) | struct { `vm`: `String` } | -| `MigrateRequest` | struct | [`MigrateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2024) | struct { `flags`: `MutationFlags` } | -| `HostPrepareRequest` | struct | [`HostPrepareRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2031) | struct { `flags`: `MutationFlags` } | -| `HostDestroyRequest` | struct | [`HostDestroyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2038) | struct { `flags`: `MutationFlags` } | -| `HostInstallRequest` | struct | [`HostInstallRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2045) | struct { `flags`: `MutationFlags`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool` } | -| `HostReconcileRequest` | struct | [`HostReconcileRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2063) | struct { `flags`: `MutationFlags`; `network`: `bool` } | -| `HostCutoverRequest` | struct | [`HostCutoverRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2074) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `candidate_id`: `Option`; `revision_plan_id`: `Option`; `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option`; `preview_digest`: `Option`; `recovery_digest`: `Option`; `operator_id`: `Option`; `consent_digest`: `Option`; `consent_json`: `Option`; `destructive_consent_digest`: `Option`; `destructive_consent_json`: `Option`; `destroy_durable_volumes`: `Option`; `recovery_attestation_json`: `Option`; `host_digest`: `Option`; `fresh_consent_digest`: `Option`; `reason`: `Option`; `reset_scope`: `Option`; `target`: `Option`; `zone`: `Option` } | -| `UsbSecurityKeyStatusRequest` | struct | [`UsbSecurityKeyStatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3892) | empty struct | -| `UsbSecurityKeySessionsRequest` | struct | [`UsbSecurityKeySessionsRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3978) | empty struct | -| `UsbSecurityKeyCancelRequest` | struct | [`UsbSecurityKeyCancelRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4032) | struct { `session_id`: `Option`; `current`: `bool` } | -| `UsbSecurityKeyTestRequest` | struct | [`UsbSecurityKeyTestRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4057) | struct { `vm`: `String` } | +| `PublicRequest` | enum | [`PublicRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L44) | `Capabilities`; `AuthStatus`; `List` - (ListRequest); `Status` - (StatusRequest); `Audit` - (AuditRequest); `HostCheck` - (HostCheckRequest); `VmStart` - (VmLifecycleRequest); `VmStop` - (VmLifecycleRequest); `VmRestart` - (VmLifecycleRequest); `Switch` - (ActivationRequest); `Boot` - (ActivationRequest); `Test` - (ActivationRequest); `Rollback` - (ActivationRequest); `Gc` - (GcRequest); `KeysList`; `KeysShow` - (KeysShowRequest); `KeysRotate` - (KeysRotateRequest); `Trust` - (TrustRequest); `RotateKnownHost` - (RotateKnownHostRequest); `UsbipBind` - (UsbipBindCliRequest); `UsbipUnbind` - (UsbipUnbindCliRequest); `UsbipProbe`; `StoreVerify` - (StoreVerifyRequest); `Migrate` - (MigrateRequest); `HostPrepare` - (HostPrepareRequest); `HostDestroy` - (HostDestroyRequest); `HostInstall` - (HostInstallRequest); `HostReconcile` - (HostReconcileRequest); `Exec` - (ExecOp); `Console` - (ConsoleOp); `Audio` - (AudioOp); `GatewayDisplay` - (GatewayDisplayOp); `Workload` - (WorkloadOp); `UsbSecurityKeyStatus`; `UsbSecurityKeySessions`; `UsbSecurityKeyCancel` - (d2b_contracts::security_key::SecurityKeyCancelRequest) | +| `ListRequest` | struct | [`ListRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L433) | struct { `env`: `Option`; `vm`: `Option` } | +| `StatusRequest` | struct | [`StatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L440) | struct { `check_bridges`: `bool`; `vm`: `Option` } | +| `AuditRequest` | struct | [`AuditRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L448) | struct { `filter`: `Option`; `format`: `AuditFormat`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | +| `HostCheckRequest` | struct | [`HostCheckRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L465) | struct { `read_only`: `bool`; `strict`: `bool` } | +| `VmLifecycleRequest` | struct | [`VmLifecycleRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L491) | struct { `vm`: `String`; `flags`: `MutationFlags`; `force`: `bool`; `no_wait_api`: `bool` } | +| `ActivationRequest` | struct | [`ActivationRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L511) | struct { `vm`: `String`; `to_generation`: `Option`; `flags`: `MutationFlags` } | +| `GcRequest` | struct | [`GcRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L522) | struct { `flags`: `MutationFlags`; `keep_generations`: `Option` } | +| `KeysShowRequest` | struct | [`KeysShowRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L531) | struct { `vm`: `String` } | +| `KeysRotateRequest` | struct | [`KeysRotateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L537) | struct { `vm`: `String`; `flags`: `MutationFlags` } | +| `TrustRequest` | struct | [`TrustRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L545) | struct { `vm`: `String`; `flags`: `MutationFlags` } | +| `RotateKnownHostRequest` | struct | [`RotateKnownHostRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L553) | struct { `vm`: `String`; `flags`: `MutationFlags` } | +| `UsbipBindCliRequest` | struct | [`UsbipBindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L561) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | +| `UsbipUnbindCliRequest` | struct | [`UsbipUnbindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L570) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | +| `StoreVerifyRequest` | struct | [`StoreVerifyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L579) | struct { `vm`: `String`; `repair`: `bool` } | +| `NamedProcessStreamRequest` | enum | [`NamedProcessStreamRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L1250) | `Stdin` - struct { `offset`: `u64`; `chunk_base64`: `String`; `eof`: `bool` }; `Read` - struct { `stream`: `ExecStream`; `offset`: `u64`; `max_len`: `u64`; `wait`: `bool`; `timeout_ms`: `u64` }; `Signal` - struct { `control_seq`: `u64`; `signo`: `u32` }; `Resize` - struct { `control_seq`: `u64`; `rows`: `u32`; `cols`: `u32` }; `CloseStdin` - struct { `offset`: `u64` }; `Cancel`; `Close`; `Wait` - struct { `timeout_ms`: `u64` } | +| `MigrateRequest` | struct | [`MigrateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2254) | struct { `flags`: `MutationFlags` } | +| `HostPrepareRequest` | struct | [`HostPrepareRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2261) | struct { `flags`: `MutationFlags` } | +| `HostDestroyRequest` | struct | [`HostDestroyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2268) | struct { `flags`: `MutationFlags` } | +| `HostInstallRequest` | struct | [`HostInstallRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2275) | struct { `flags`: `MutationFlags`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool` } | +| `HostReconcileRequest` | struct | [`HostReconcileRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2293) | struct { `flags`: `MutationFlags`; `network`: `bool` } | +| `UsbSecurityKeyStatusRequest` | struct | [`UsbSecurityKeyStatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4056) | empty struct | +| `UsbSecurityKeySessionsRequest` | struct | [`UsbSecurityKeySessionsRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4142) | empty struct | +| `UsbSecurityKeyCancelRequest` | struct | [`UsbSecurityKeyCancelRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4196) | struct { `session_id`: `Option`; `current`: `bool` } | +| `UsbSecurityKeyTestRequest` | struct | [`UsbSecurityKeyTestRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4221) | struct { `vm`: `String` } | ### Broker socket request types | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `BrokerRequest` | enum | [`BrokerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L35) | `ApplyHostGenerationHandoff` - (crate::host_generation::ApplyHostGenerationHandoff); `LaunchCutoverRunner` - (LaunchCutoverRunnerRequest); `CutoverAudit` - (CutoverAuditRequest); `CutoverEffect` - (CutoverEffectRequest); `ApplyNftables` - (ApplyNftablesRequest); `ApplyNftablesProjection` - (ApplyNftablesProjectionRequest); `ApplyNmUnmanaged` - (ApplyNmUnmanagedRequest); `ApplyRoute` - (ApplyRouteRequest); `ApplySysctl` - (ApplySysctlRequest); `BindUnixSocket` - (BindUnixSocketRequest); `CreateOrReconcileUsersGroups` - (CreateOrReconcileUsersGroupsRequest); `CreateBridge` - (CreateBridgeRequest); `DeleteBridge` - (DeleteBridgeRequest); `CreatePersistentTap` - (CreatePersistentTapRequest); `DeletePersistentTap` - (DeletePersistentTapRequest); `CreateTapFd` - (CreateTapFdRequest); `DelegateCgroupV2` - (DelegateCgroupV2Request); `ExportBrokerAudit` - (ExportBrokerAuditRequest); `Hello` - (HelloRequest); `GuestControlSign` - (GuestControlSignRequest); `InjectSecretById` - (SecretByIdRequest); `LaunchMinijailChild` - (LaunchMinijailChildRequest); `ModprobeIfAllowed` - (ModprobeIfAllowedRequest); `OpenCgroupDir` - (OpenCgroupDirRequest); `OpenDevice` - (OpenDeviceRequest); `OpenFuse` - (OpenFuseRequest); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyRequest); `OpenKvm` - (OpenKvmRequest); `QemuMediaEnroll` - (QemuMediaEnrollRequest); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryRequest); `QemuMediaBoot` - (QemuMediaBootRequest); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleRequest); `QemuMediaQueryStatus` - (QemuMediaQueryStatusRequest); `QemuMediaQuit` - (QemuMediaLifecycleRequest); `QemuMediaAttach` - (QemuMediaHotplugRequest); `QemuMediaDetach` - (QemuMediaHotplugRequest); `OpenPidfd` - (OpenPidfdRequest); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketRequest); `ObserveRunner` - (ObserveRunnerRequest); `PipeWireAudio` - (PipeWireAudioRequest); `StartSystemdUnit` - (StartSystemdUnitRequest); `CheckSystemdUserManager` - (CheckSystemdUserManagerRequest); `ObserveSystemdUnit` - (ObserveSystemdUnitRequest); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdRequest); `StopSystemdUnit` - (StopSystemdUnitRequest); `OpenZoneStore` - (OpenZoneStoreRequest); `OpenVhostNet` - (OpenVhostNetRequest); `PauseBroker`; `PollChildReaped`; `PrepareRuntimeDir` - (PrepareDirRequest); `PrepareStateDir` - (PrepareDirRequest); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateRequest); `ReconcileStorageScope` - (ReconcileStorageScopeRequest); `ValidateLockSpec` - (ValidateLockSpecRequest); `PrepareStoreView` - (PrepareStoreViewRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `ReadSecretById` - (SecretByIdRequest); `ResumeBroker`; `RotateSecretById` - (SecretByIdRequest); `RunHostInstall` - (RunHostInstallRequest); `RunMigrate` - (RunMigrateRequest); `RunActivation` - (RunActivationRequest); `RunGc` - (RunGcRequest); `RunKeysRotate` - (RunKeysRotateRequest); `RunHostKeyTrust` - (RunHostKeyTrustRequest); `RunRotateKnownHost` - (RunRotateKnownHostRequest); `SetBridgePortFlags` - (SetBridgePortFlagsRequest); `SetSocketAcl` - (SetSocketAclRequest); `SetupMountNamespace` - (SetupMountNamespaceRequest); `CgroupKill` - (CgroupKillRequest); `SignalRunner` - (SignalRunnerRequest); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdRequest); `SpawnRunner` - (SpawnRunnerRequest); `UpdateHostsFile` - (UpdateHostsFileRequest); `UsbipBind` - (UsbipBindRequest); `UsbipBindFirewallRule` - (UsbipBindFirewallRuleRequest); `UsbipProxyReconcile` - (UsbipProxyReconcileRequest); `UsbipUnbind` - (UsbipUnbindRequest); `UsbipExplicitBind` - (UsbipExplicitBindRequest); `UsbipExplicitFirewallRule` - (UsbipExplicitFirewallRuleRequest); `ResourceActivationAudit` - (ResourceActivationAuditRequest); `ValidateBundle`; `SeedDnsmasqLease` - (SeedDnsmasqLeaseRequest); `BindMountFromHardlinkFarm` - (BindMountFromHardlinkFarmRequest); `OwnershipMatrixCheck` - (OwnershipMatrixCheckRequest); `SshHostKeyPreflight` - (SshHostKeyPreflightRequest); `DiskInit` - (DiskInitRequest); `SecurityKeyOpenDevice` - (d2b_contracts::security_key::SecurityKeyOpenDeviceRequest); `SecurityKeyApplyUdevRules` - (d2b_contracts::security_key::SecurityKeyApplyUdevRulesRequest) | -| `LaunchCutoverRunnerRequest` | struct | [`LaunchCutoverRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L344) | struct { `operation_id`: `BundleOpId`; `bootstrap_fd_index`: `u32`; `capability_digest`: `CanonicalAuditDigest`; `expires_at_ms`: `u64` } | -| `CutoverAuditRequest` | struct | [`CutoverAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L409) | struct { `operation_id`: `BundleOpId`; `phase`: `u8`; `transition`: `CutoverAuditTransition`; `request_digest`: `CanonicalAuditDigest`; `reason_digest`: `Option` } | -| `CutoverVerificationRequest` | struct | [`CutoverVerificationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L506) | struct { `expected_zone_ids`: `Vec` } | -| `CutoverAdmissionRequest` | struct | [`CutoverAdmissionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L543) | struct { `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option` } | -| `CutoverEffectRequest` | struct | [`CutoverEffectRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L604) | struct { `operation_id`: `BundleOpId`; `authority`: `CutoverEffectAuthority`; `phase`: `u8`; `effect_id`: `BundleOpId`; `effect`: `CutoverEffectKind`; `replay_class`: `CutoverReplayClass`; `request_digest`: `CanonicalAuditDigest`; `capability_digest`: `CanonicalAuditDigest`; `identity`: `Option`; `handoff`: `Option`; `payload`: `Option` } | -| `RunHostInstallRequest` | struct | [`RunHostInstallRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1282) | struct { `bundle_installer_intent_ref`: `BundleOpId`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool`; `tracing_span_id`: `Option` } | -| `RunMigrateRequest` | struct | [`RunMigrateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1306) | struct { `bundle_migrate_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `RunActivationRequest` | struct | [`RunActivationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1339) | struct { `bundle_activation_intent_ref`: `BundleOpId`; `mode`: `ActivationMode`; `system_artifact_id`: `Option`; `phase`: `ActivationPhase`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunGcRequest` | struct | [`RunGcRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1366) | struct { `bundle_gc_intent_ref`: `BundleOpId`; `keep_generations`: `Option`; `tracing_span_id`: `Option` } | -| `RunKeysRotateRequest` | struct | [`RunKeysRotateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1385) | struct { `bundle_keys_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunHostKeyTrustRequest` | struct | [`RunHostKeyTrustRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1402) | struct { `bundle_trust_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunRotateKnownHostRequest` | struct | [`RunRotateKnownHostRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1420) | struct { `bundle_rotate_known_host_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1443) | struct { `client_version`: `String`; `supported_features`: `Vec` } | -| `ApplyNftablesRequest` | struct | [`ApplyNftablesRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1594) | struct { `bundle_nft_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `desired_hash`: `Option`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplyNftablesProjectionRequest` | struct | [`ApplyNftablesProjectionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1625) | struct { `bundle_nft_projection_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `action`: `NftablesProjectionAction`; `expected_generation_id`: `ResourceBundleGenerationId`; `desired_hash`: `Option`; `tracing_span_id`: `Option` } | -| `ApplyNmUnmanagedRequest` | struct | [`ApplyNmUnmanagedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1639) | struct { `bundle_nm_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplyRouteRequest` | struct | [`ApplyRouteRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1650) | struct { `bundle_route_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplySysctlRequest` | struct | [`ApplySysctlRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1661) | struct { `bundle_sysctl_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `BindUnixSocketRequest` | struct | [`BindUnixSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1672) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `CreateOrReconcileUsersGroupsRequest` | struct | [`CreateOrReconcileUsersGroupsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1682) | struct { `subject_ids`: `Vec`; `tracing_span_id`: `Option` } | -| `CreatePersistentTapRequest` | struct | [`CreatePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1696) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `attachment_id`: `Option`; `network_generation`: `Option`; `attachment_generation`: `Option`; `tracing_span_id`: `Option` } | -| `DeletePersistentTapRequest` | struct | [`DeletePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1716) | struct { `attachment_id`: `ResourceUid`; `expected_network_generation`: `ResourceGeneration`; `expected_attachment_generation`: `ResourceGeneration`; `tracing_span_id`: `Option` } | -| `CreateBridgeRequest` | struct | [`CreateBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1730) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `DeleteBridgeRequest` | struct | [`DeleteBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1741) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `CreateTapFdRequest` | struct | [`CreateTapFdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1752) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `DelegateCgroupV2Request` | struct | [`DelegateCgroupV2Request`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1764) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `ExportBrokerAuditRequest` | struct | [`ExportBrokerAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1772) | struct { `filter`: `Option`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | -| `GuestControlSignRequest` | struct | [`GuestControlSignRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1882) | struct { `vm_id`: `VmId`; `role`: `GuestControlProofRole`; `protocol_version`: `u32`; `direction`: `GuestControlDirection`; `purpose`: `GuestControlAuthPurpose`; `guest_control_port`: `u32`; `peer_cid`: `Option`; `host_nonce`: `Vec`; `guest_nonce`: `Vec`; `guest_boot_id`: `GuestBootIdWire`; `capabilities_hash`: `Option`; `tracing_span_id`: `Option` } | -| `SecretByIdRequest` | struct | [`SecretByIdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1937) | struct { `opaque_id`: `String`; `tracing_span_id`: `Option` } | -| `LaunchMinijailChildRequest` | struct | [`LaunchMinijailChildRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1949) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `ModprobeIfAllowedRequest` | struct | [`ModprobeIfAllowedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1963) | struct { `module_name`: `String`; `tracing_span_id`: `Option` } | -| `OpenCgroupDirRequest` | struct | [`OpenCgroupDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1973) | struct { `scope_id`: `ScopeId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | -| `OpenDeviceRequest` | struct | [`OpenDeviceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1982) | struct { `role_id`: `RoleId`; `device_class`: `String`; `tracing_span_id`: `Option` } | -| `OpenKvmRequest` | struct | [`OpenKvmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1991) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `QemuMediaEnrollRequest` | struct | [`QemuMediaEnrollRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2005) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `bus_id`: `String`; `tracing_span_id`: `Option` } | -| `QemuMediaRefreshRegistryRequest` | struct | [`QemuMediaRefreshRegistryRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2026) | struct { `tracing_span_id`: `Option` } | -| `QemuMediaBootRequest` | struct | [`QemuMediaBootRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2048) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `QemuMediaLifecycleRequest` | struct | [`QemuMediaLifecycleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2056) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `QemuMediaQueryStatusRequest` | struct | [`QemuMediaQueryStatusRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2064) | struct { `vm_id`: `VmId`; `shutdown_context`: `bool`; `tracing_span_id`: `Option` } | -| `QemuMediaHotplugRequest` | struct | [`QemuMediaHotplugRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2129) | struct { `vm_id`: `VmId`; `bus_id`: `String`; `tracing_span_id`: `Option` } | -| `OpenPidfdRequest` | struct | [`OpenPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2187) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `expected_start_time_ticks`: `u64`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `OpenPeerPidfdFromAcceptedSocketRequest` | struct | [`OpenPeerPidfdFromAcceptedSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2230) | empty struct | -| `ObserveRunnerRequest` | struct | [`ObserveRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2245) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `PipeWireAudioRequest` | struct | [`PipeWireAudioRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2295) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `bundle_runner_intent_ref`: `BundleOpId`; `channel`: `PipeWireAudioChannel`; `action`: `PipeWireAudioAction`; `tracing_span_id`: `Option` } | -| `SystemdUnitRequest` | struct | [`SystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2362) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `bundle_content_identity`: `String`; `provider_identity`: `[u8; 32]`; `template_identity`: `[u8; 32]`; `generation`: `u64`; `domain`: `SystemdUnitDomain`; `execution_ref`: `Option`; `user_ref`: `Option`; `sandbox_plan`: `Option`; `tracing_span_id`: `Option` } | -| `OpenSystemdUnitPidfdRequest` | struct | [`OpenSystemdUnitPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2415) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity` } | -| `StopSystemdUnitRequest` | struct | [`StopSystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2426) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity`; `class`: `SystemdStopClass` } | -| `OpenZoneStoreRequest` | struct | [`OpenZoneStoreRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2502) | struct { `zone_store_id`: `ZoneStoreId` } | -| `OpenVhostNetRequest` | struct | [`OpenVhostNetRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2536) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `OpenFuseRequest` | struct | [`OpenFuseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2544) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `OpenHidrawSecurityKeyRequest` | struct | [`OpenHidrawSecurityKeyRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2556) | struct { `vm_id`: `VmId`; `selector_id`: `String`; `device_ref`: `ResourceRef`; `authority_key`: `String`; `tracing_span_id`: `Option` } | -| `PrepareDirRequest` | struct | [`PrepareDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2599) | struct { `vm_id`: `VmId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | -| `MigrateLegacySwtpmStateRequest` | struct | [`MigrateLegacySwtpmStateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2609) | struct { `bundle_legacy_swtpm_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `probe_only`: `bool`; `tracing_span_id`: `Option` } | -| `PrepareStoreViewRequest` | struct | [`PrepareStoreViewRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2662) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `StoreSyncRequest` | struct | [`StoreSyncRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2682) | struct { `vm_id`: `VmId`; `bundle_closure_ref`: `BundleClosureRef`; `generation_token`: `u32`; `tracing_span_id`: `Option` } | -| `SetBridgePortFlagsRequest` | struct | [`SetBridgePortFlagsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2726) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `SetSocketAclRequest` | struct | [`SetSocketAclRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2735) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `SetupMountNamespaceRequest` | struct | [`SetupMountNamespaceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2745) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `UpdateHostsFileRequest` | struct | [`UpdateHostsFileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2756) | struct { `bundle_hosts_intent_ref`: `BundleOpId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `UsbipBindRequest` | struct | [`UsbipBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2769) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `UsbipBindFirewallRuleRequest` | struct | [`UsbipBindFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2786) | struct { `bundle_usbip_firewall_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `UsbipProxyReconcileRequest` | struct | [`UsbipProxyReconcileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2794) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `UsbipUnbindRequest` | struct | [`UsbipUnbindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2802) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `preserve_durable_claim`: `bool`; `tracing_span_id`: `Option` } | -| `UsbipExplicitBindRequest` | struct | [`UsbipExplicitBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2824) | struct { `bus_id`: `String`; `vm`: `String`; `env`: `String`; `tracing_span_id`: `Option` } | -| `UsbipExplicitFirewallRuleRequest` | struct | [`UsbipExplicitFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2842) | struct { `bus_id`: `String`; `env`: `String`; `host_uplink_ip`: `String`; `net_uplink_ip`: `String`; `tracing_span_id`: `Option` } | -| `ResourceActivationAuditRequest` | struct | [`ResourceActivationAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2934) | struct { `audit_join`: `AuditJoinContext` } | -| `SignalRunnerRequest` | struct | [`SignalRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2965) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `signal`: `RunnerSignal`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `CgroupKillRequest` | struct | [`CgroupKillRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2992) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `DeregisterRunnerPidfdRequest` | struct | [`DeregisterRunnerPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3001) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `SpawnRunnerRequest` | struct | [`SpawnRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3120) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `bundle_content_identity`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `sandbox_plan`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `runtime_allocations`: `Vec`; `tracing_span_id`: `Option`; `workload_identity`: `Option` } | -| `SeedDnsmasqLeaseRequest` | struct | [`SeedDnsmasqLeaseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3395) | struct { `vm_id`: `VmId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `BindMountFromHardlinkFarmRequest` | struct | [`BindMountFromHardlinkFarmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3407) | struct { `vm_id`: `VmId`; `bundle_store_view_intent_ref`: `Option`; `tracing_span_id`: `Option` } | -| `OwnershipMatrixCheckRequest` | struct | [`OwnershipMatrixCheckRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3422) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `SshHostKeyPreflightRequest` | struct | [`SshHostKeyPreflightRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3433) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `ReconcileStorageScopeRequest` | struct | [`ReconcileStorageScopeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3447) | struct { `storage_ref`: `BundleOpId`; `apply`: `bool`; `tracing_span_id`: `Option` } | -| `ValidateLockSpecRequest` | struct | [`ValidateLockSpecRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3484) | struct { `lock_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `DiskInitRequest` | struct | [`DiskInitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3518) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `BrokerRequest` | enum | [`BrokerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L35) | `ApplyHostGenerationHandoff` - (crate::host_generation::ApplyHostGenerationHandoff); `ApplyNftables` - (ApplyNftablesRequest); `ApplyNftablesProjection` - (ApplyNftablesProjectionRequest); `ApplyNmUnmanaged` - (ApplyNmUnmanagedRequest); `ApplyRoute` - (ApplyRouteRequest); `ApplySysctl` - (ApplySysctlRequest); `BindUnixSocket` - (BindUnixSocketRequest); `CreateOrReconcileUsersGroups` - (CreateOrReconcileUsersGroupsRequest); `CreateBridge` - (CreateBridgeRequest); `DeleteBridge` - (DeleteBridgeRequest); `CreatePersistentTap` - (CreatePersistentTapRequest); `DeletePersistentTap` - (DeletePersistentTapRequest); `CreateTapFd` - (CreateTapFdRequest); `DelegateCgroupV2` - (DelegateCgroupV2Request); `ExportBrokerAudit` - (ExportBrokerAuditRequest); `Hello` - (HelloRequest); `InjectSecretById` - (SecretByIdRequest); `LaunchMinijailChild` - (LaunchMinijailChildRequest); `ModprobeIfAllowed` - (ModprobeIfAllowedRequest); `OpenCgroupDir` - (OpenCgroupDirRequest); `OpenDevice` - (OpenDeviceRequest); `OpenFuse` - (OpenFuseRequest); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyRequest); `OpenKvm` - (OpenKvmRequest); `QemuMediaEnroll` - (QemuMediaEnrollRequest); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryRequest); `QemuMediaBoot` - (QemuMediaBootRequest); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleRequest); `QemuMediaQueryStatus` - (QemuMediaQueryStatusRequest); `QemuMediaQuit` - (QemuMediaLifecycleRequest); `QemuMediaAttach` - (QemuMediaHotplugRequest); `QemuMediaDetach` - (QemuMediaHotplugRequest); `OpenPidfd` - (OpenPidfdRequest); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketRequest); `ObserveRunner` - (ObserveRunnerRequest); `PipeWireAudio` - (PipeWireAudioRequest); `StartSystemdUnit` - (StartSystemdUnitRequest); `CheckSystemdUserManager` - (CheckSystemdUserManagerRequest); `ObserveSystemdUnit` - (ObserveSystemdUnitRequest); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdRequest); `StopSystemdUnit` - (StopSystemdUnitRequest); `OpenZoneStore` - (OpenZoneStoreRequest); `OpenVhostNet` - (OpenVhostNetRequest); `PauseBroker`; `PollChildReaped`; `PrepareRuntimeDir` - (PrepareDirRequest); `PrepareStateDir` - (PrepareDirRequest); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateRequest); `ReconcileStorageScope` - (ReconcileStorageScopeRequest); `ValidateLockSpec` - (ValidateLockSpecRequest); `PrepareStoreView` - (PrepareStoreViewRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `ReadSecretById` - (SecretByIdRequest); `ResumeBroker`; `RotateSecretById` - (SecretByIdRequest); `RunHostInstall` - (RunHostInstallRequest); `RunMigrate` - (RunMigrateRequest); `RunActivation` - (RunActivationRequest); `RunGc` - (RunGcRequest); `RunKeysRotate` - (RunKeysRotateRequest); `RunHostKeyTrust` - (RunHostKeyTrustRequest); `RunRotateKnownHost` - (RunRotateKnownHostRequest); `SetBridgePortFlags` - (SetBridgePortFlagsRequest); `SetSocketAcl` - (SetSocketAclRequest); `SetupMountNamespace` - (SetupMountNamespaceRequest); `CgroupKill` - (CgroupKillRequest); `SignalRunner` - (SignalRunnerRequest); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdRequest); `SpawnRunner` - (SpawnRunnerRequest); `UpdateHostsFile` - (UpdateHostsFileRequest); `UsbipBind` - (UsbipBindRequest); `UsbipBindFirewallRule` - (UsbipBindFirewallRuleRequest); `UsbipProxyReconcile` - (UsbipProxyReconcileRequest); `UsbipUnbind` - (UsbipUnbindRequest); `UsbipExplicitBind` - (UsbipExplicitBindRequest); `UsbipExplicitFirewallRule` - (UsbipExplicitFirewallRuleRequest); `ResourceActivationAudit` - (ResourceActivationAuditRequest); `ValidateBundle`; `SeedDnsmasqLease` - (SeedDnsmasqLeaseRequest); `BindMountFromHardlinkFarm` - (BindMountFromHardlinkFarmRequest); `OwnershipMatrixCheck` - (OwnershipMatrixCheckRequest); `SshHostKeyPreflight` - (SshHostKeyPreflightRequest); `DiskInit` - (DiskInitRequest); `SecurityKeyOpenDevice` - (d2b_contracts::security_key::SecurityKeyOpenDeviceRequest); `SecurityKeyApplyUdevRules` - (d2b_contracts::security_key::SecurityKeyApplyUdevRulesRequest) | +| `RunHostInstallRequest` | struct | [`RunHostInstallRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1220) | struct { `bundle_installer_intent_ref`: `BundleOpId`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool`; `tracing_span_id`: `Option` } | +| `RunMigrateRequest` | struct | [`RunMigrateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1244) | struct { `bundle_migrate_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `RunActivationRequest` | struct | [`RunActivationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1277) | struct { `bundle_activation_intent_ref`: `BundleOpId`; `mode`: `ActivationMode`; `system_artifact_id`: `Option`; `phase`: `ActivationPhase`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunGcRequest` | struct | [`RunGcRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1304) | struct { `bundle_gc_intent_ref`: `BundleOpId`; `keep_generations`: `Option`; `tracing_span_id`: `Option` } | +| `RunKeysRotateRequest` | struct | [`RunKeysRotateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1323) | struct { `bundle_keys_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunHostKeyTrustRequest` | struct | [`RunHostKeyTrustRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1340) | struct { `bundle_trust_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunRotateKnownHostRequest` | struct | [`RunRotateKnownHostRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1358) | struct { `bundle_rotate_known_host_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1381) | struct { `client_version`: `String`; `supported_features`: `Vec` } | +| `ApplyNftablesRequest` | struct | [`ApplyNftablesRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1525) | struct { `bundle_nft_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `desired_hash`: `Option`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplyNftablesProjectionRequest` | struct | [`ApplyNftablesProjectionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1556) | struct { `bundle_nft_projection_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `action`: `NftablesProjectionAction`; `expected_generation_id`: `ResourceBundleGenerationId`; `desired_hash`: `Option`; `tracing_span_id`: `Option` } | +| `ApplyNmUnmanagedRequest` | struct | [`ApplyNmUnmanagedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1570) | struct { `bundle_nm_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplyRouteRequest` | struct | [`ApplyRouteRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1581) | struct { `bundle_route_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplySysctlRequest` | struct | [`ApplySysctlRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1592) | struct { `bundle_sysctl_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `BindUnixSocketRequest` | struct | [`BindUnixSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1603) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `CreateOrReconcileUsersGroupsRequest` | struct | [`CreateOrReconcileUsersGroupsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1613) | struct { `subject_ids`: `Vec`; `tracing_span_id`: `Option` } | +| `CreatePersistentTapRequest` | struct | [`CreatePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1627) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `attachment_id`: `Option`; `network_generation`: `Option`; `attachment_generation`: `Option`; `tracing_span_id`: `Option` } | +| `DeletePersistentTapRequest` | struct | [`DeletePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1647) | struct { `attachment_id`: `ResourceUid`; `expected_network_generation`: `ResourceGeneration`; `expected_attachment_generation`: `ResourceGeneration`; `tracing_span_id`: `Option` } | +| `CreateBridgeRequest` | struct | [`CreateBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1661) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `DeleteBridgeRequest` | struct | [`DeleteBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1672) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `CreateTapFdRequest` | struct | [`CreateTapFdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1683) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `DelegateCgroupV2Request` | struct | [`DelegateCgroupV2Request`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1695) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `ExportBrokerAuditRequest` | struct | [`ExportBrokerAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1703) | struct { `filter`: `Option`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | +| `SecretByIdRequest` | struct | [`SecretByIdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1760) | struct { `opaque_id`: `String`; `tracing_span_id`: `Option` } | +| `LaunchMinijailChildRequest` | struct | [`LaunchMinijailChildRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1772) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `ModprobeIfAllowedRequest` | struct | [`ModprobeIfAllowedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1786) | struct { `module_name`: `String`; `tracing_span_id`: `Option` } | +| `OpenCgroupDirRequest` | struct | [`OpenCgroupDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1796) | struct { `scope_id`: `ScopeId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | +| `OpenDeviceRequest` | struct | [`OpenDeviceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1805) | struct { `role_id`: `RoleId`; `device_class`: `String`; `tracing_span_id`: `Option` } | +| `OpenKvmRequest` | struct | [`OpenKvmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1814) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `QemuMediaEnrollRequest` | struct | [`QemuMediaEnrollRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1828) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `bus_id`: `String`; `tracing_span_id`: `Option` } | +| `QemuMediaRefreshRegistryRequest` | struct | [`QemuMediaRefreshRegistryRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1849) | struct { `tracing_span_id`: `Option` } | +| `QemuMediaBootRequest` | struct | [`QemuMediaBootRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1871) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `QemuMediaLifecycleRequest` | struct | [`QemuMediaLifecycleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1879) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `QemuMediaQueryStatusRequest` | struct | [`QemuMediaQueryStatusRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1887) | struct { `vm_id`: `VmId`; `shutdown_context`: `bool`; `tracing_span_id`: `Option` } | +| `QemuMediaHotplugRequest` | struct | [`QemuMediaHotplugRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1952) | struct { `vm_id`: `VmId`; `bus_id`: `String`; `tracing_span_id`: `Option` } | +| `OpenPidfdRequest` | struct | [`OpenPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2039) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `expected_start_time_ticks`: `u64`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `OpenPeerPidfdFromAcceptedSocketRequest` | struct | [`OpenPeerPidfdFromAcceptedSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2085) | empty struct | +| `ObserveRunnerRequest` | struct | [`ObserveRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2100) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `PipeWireAudioRequest` | struct | [`PipeWireAudioRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2153) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `bundle_runner_intent_ref`: `BundleOpId`; `channel`: `PipeWireAudioChannel`; `action`: `PipeWireAudioAction`; `tracing_span_id`: `Option` } | +| `SystemdUnitRequest` | struct | [`SystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2223) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `bundle_content_identity`: `String`; `provider_identity`: `[u8; 32]`; `template_identity`: `[u8; 32]`; `generation`: `u64`; `domain`: `SystemdUnitDomain`; `execution_ref`: `Option`; `user_ref`: `Option`; `guest_execution`: `Option`; `sandbox_plan`: `Option`; `tracing_span_id`: `Option` } | +| `OpenSystemdUnitPidfdRequest` | struct | [`OpenSystemdUnitPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2279) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity` } | +| `StopSystemdUnitRequest` | struct | [`StopSystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2290) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity`; `class`: `SystemdStopClass` } | +| `OpenZoneStoreRequest` | struct | [`OpenZoneStoreRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2366) | struct { `zone_store_id`: `ZoneStoreId` } | +| `OpenVhostNetRequest` | struct | [`OpenVhostNetRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2400) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `OpenFuseRequest` | struct | [`OpenFuseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2408) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `OpenHidrawSecurityKeyRequest` | struct | [`OpenHidrawSecurityKeyRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2420) | struct { `vm_id`: `VmId`; `selector_id`: `String`; `device_ref`: `ResourceRef`; `authority_key`: `String`; `tracing_span_id`: `Option` } | +| `PrepareDirRequest` | struct | [`PrepareDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2463) | struct { `vm_id`: `VmId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | +| `MigrateLegacySwtpmStateRequest` | struct | [`MigrateLegacySwtpmStateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2473) | struct { `bundle_legacy_swtpm_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `probe_only`: `bool`; `tracing_span_id`: `Option` } | +| `PrepareStoreViewRequest` | struct | [`PrepareStoreViewRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2526) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `StoreSyncRequest` | struct | [`StoreSyncRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2546) | struct { `vm_id`: `VmId`; `bundle_closure_ref`: `BundleClosureRef`; `generation_token`: `u32`; `tracing_span_id`: `Option` } | +| `SetBridgePortFlagsRequest` | struct | [`SetBridgePortFlagsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2590) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `SetSocketAclRequest` | struct | [`SetSocketAclRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2599) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `SetupMountNamespaceRequest` | struct | [`SetupMountNamespaceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2609) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `UpdateHostsFileRequest` | struct | [`UpdateHostsFileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2620) | struct { `bundle_hosts_intent_ref`: `BundleOpId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `UsbipBindRequest` | struct | [`UsbipBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2633) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `UsbipBindFirewallRuleRequest` | struct | [`UsbipBindFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2650) | struct { `bundle_usbip_firewall_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `UsbipProxyReconcileRequest` | struct | [`UsbipProxyReconcileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2658) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `UsbipUnbindRequest` | struct | [`UsbipUnbindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2666) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `preserve_durable_claim`: `bool`; `tracing_span_id`: `Option` } | +| `UsbipExplicitBindRequest` | struct | [`UsbipExplicitBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2688) | struct { `bus_id`: `String`; `vm`: `String`; `env`: `String`; `tracing_span_id`: `Option` } | +| `UsbipExplicitFirewallRuleRequest` | struct | [`UsbipExplicitFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2706) | struct { `bus_id`: `String`; `env`: `String`; `host_uplink_ip`: `String`; `net_uplink_ip`: `String`; `tracing_span_id`: `Option` } | +| `ResourceActivationAuditRequest` | struct | [`ResourceActivationAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2798) | struct { `audit_join`: `AuditJoinContext` } | +| `SignalRunnerRequest` | struct | [`SignalRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2829) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `signal`: `RunnerSignal`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `CgroupKillRequest` | struct | [`CgroupKillRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2859) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `DeregisterRunnerPidfdRequest` | struct | [`DeregisterRunnerPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2868) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `SpawnRunnerRequest` | struct | [`SpawnRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3002) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `bundle_content_identity`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `activation_input`: `Option`; `sandbox_plan`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `guest_execution`: `Option`; `runtime_allocations`: `Vec`; `tracing_span_id`: `Option`; `workload_identity`: `Option` } | +| `SeedDnsmasqLeaseRequest` | struct | [`SeedDnsmasqLeaseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3274) | struct { `vm_id`: `VmId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `BindMountFromHardlinkFarmRequest` | struct | [`BindMountFromHardlinkFarmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3286) | struct { `vm_id`: `VmId`; `bundle_store_view_intent_ref`: `Option`; `tracing_span_id`: `Option` } | +| `OwnershipMatrixCheckRequest` | struct | [`OwnershipMatrixCheckRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3301) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `SshHostKeyPreflightRequest` | struct | [`SshHostKeyPreflightRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3312) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `ReconcileStorageScopeRequest` | struct | [`ReconcileStorageScopeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3326) | struct { `storage_ref`: `BundleOpId`; `apply`: `bool`; `tracing_span_id`: `Option` } | +| `ValidateLockSpecRequest` | struct | [`ValidateLockSpecRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3363) | struct { `lock_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `DiskInitRequest` | struct | [`DiskInitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3397) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | ### Console and audio wire types @@ -382,8 +373,8 @@ The implemented surface contracts are: `Status` returns per-target `AudioProviderResult` structs so one misconfigured provider does not fail the entire multi-target query. Volume and gain values are bounded `0..=100` domain integers validated - at the wire boundary. Host PipeWire enforcement and guestd `AudioSet` / - `AudioStatus` integration report `host-and-guest` only after both live + at the wire boundary. Host PipeWire enforcement and target-local Process + `AudioSet` / `AudioStatus` integration report `host-and-guest` only after both live enforcement paths complete; qemu-media reports the intentional `host-only` posture because guest enforcement is unsupported there. @@ -397,80 +388,73 @@ see the auto-generated tables above for the committed Rust variants. | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `PublicResponse` | enum | [`PublicResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L149) | `Capabilities` - (CapabilitiesResponse); `AuthStatus` - (AuthStatusResponse); `List` - (ListResponse); `Status` - (StatusResponse); `Audit` - (AuditResponse); `HostCheck` - (HostCheckResponse); `HostCutover` - (HostCutoverResponse); `KeysList` - (KeysListResponse); `KeysShow` - (KeysShowResponse); `UsbipProbe` - (UsbipProbeResponse); `StoreVerify` - (StoreVerifyResponse); `MutatingVerb` - (MutatingVerbResponse); `ReadGuestConfig` - (ReadGuestConfigResponse); `Exec` - (ExecOpResponse); `Console` - (ConsoleOpResponse); `Audio` - (AudioOpResponse); `GatewayDisplay` - (GatewayDisplayOpResponse); `Workload` - (WorkloadOpResponse); `UsbSecurityKeyStatus` - (d2b_contracts::security_key::SecurityKeyStatusResponse); `UsbSecurityKeySessions` - (d2b_contracts::security_key::SecurityKeySessionsResponse); `UsbSecurityKeyCancel` - (d2b_contracts::security_key::SecurityKeyCancelResponse); `Error` - (Error) | -| `WorkloadOpResponse` | enum | [`WorkloadOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L215) | `List` - (WorkloadListResult); `Status` - (Box); `LauncherExec` - (LauncherExecResult) | -| `GatewayDisplayOpResponse` | enum | [`GatewayDisplayOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L370) | `Start` - (GatewayDisplayStartResult); `Stop` - (GatewayDisplayStopResult); `Open` - (GatewayDisplayOpenResult); `Close` - (GatewayDisplayCloseResult); `List` - (GatewayDisplayListResult); `ListDetailed` - (GatewayDisplayListDetailedResult) | -| `ReadGuestConfigResponse` | struct | [`ReadGuestConfigResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L606) | struct { `content_base64`: `String` } | -| `ExecOpResponse` | enum | [`ExecOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1246) | `Start` - (ExecStartResult); `DetachedCreate` - (ExecDetachedCreateResult); `WriteStdin` - (ExecWriteStdinResult); `ReadOutput` - (ExecReadOutputResult); `Signal` - (ExecControlResult); `Resize` - (ExecControlResult); `Wait` - (ExecWaitResult); `Close` - (ExecCloseResult); `List` - (ExecDetachedListResult); `Logs` - (ExecDetachedLogsResult); `Status` - (ExecDetachedStatusResult); `Kill` - (ExecDetachedKillResult) | -| `ConsoleOpResponse` | enum | [`ConsoleOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1808) | `Attach` - (ConsoleAttachResult); `WriteStdin` - (ConsoleControlResult); `ReadOutput` - (ConsoleReadOutputResult); `Resize` - (ConsoleControlResult); `Wait` - (ConsoleWaitResult); `Close` - (ConsoleCloseResult) | -| `AudioOpResponse` | enum | [`AudioOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2014) | `Status` - (AudioStatusResult); `SetVolume` - (AudioSetResult); `Mute` - (AudioSetResult) | -| `HostCutoverResponse` | struct | [`HostCutoverResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2180) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `state`: `String`; `phase`: `u8`; `preview_digest`: `Option`; `summary`: `String`; `mutation_accepted`: `bool`; `inventory`: `Option` } | -| `MutatingVerbResponse` | struct | [`MutatingVerbResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2241) | struct { `verb`: `String`; `outcome`: `MutatingVerbOutcome`; `target_wave`: `Option`; `summary`: `Option`; `remediation`: `Option`; `api_ready`: `Option` } | -| `CapabilitiesResponse` | struct | [`CapabilitiesResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2267) | struct { `broker_socket`: `String`; `capabilities`: `Vec`; `public_socket`: `String`; `server_version`: `Version`; `selected_version`: `Version` } | -| `AuthStatusResponse` | struct | [`AuthStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2277) | struct { `allowed_subcommands`: `Vec`; `denied_subcommands`: `Vec`; `role`: `AuthRole`; `sockets`: `Vec` } | -| `ListResponse` | struct | [`ListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2286) | struct { `vms`: `Vec`; `read_model`: `Option` } | -| `StatusResponse` | struct | [`StatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2294) | struct { `entries`: `Vec`; `read_model`: `Option` } | -| `AuditResponse` | struct | [`AuditResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2314) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | -| `HostCheckResponse` | struct | [`HostCheckResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2364) | struct { `exit_code`: `u8`; `findings`: `Vec` } | -| `KeysListResponse` | struct | [`KeysListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2382) | struct { `entries`: `Vec` } | -| `KeysShowResponse` | struct | [`KeysShowResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2388) | struct { `vm`: `String`; `env`: `Option`; `managed_key_path`: `String`; `public_key`: `String`; `fingerprint`: `String`; `known_hosts_entry`: `Option` } | -| `UsbipProbeResponse` | struct | [`UsbipProbeResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2629) | struct { `entries`: `Vec` } | -| `UsbSecurityKeyStatusResponse` | struct | [`UsbSecurityKeyStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L3963) | struct { `host_proxy_enabled`: `bool`; `physical_keys`: `Vec`; `vm_devices`: `Vec`; `lease`: `UsbSkLeaseStatus` } | -| `UsbSecurityKeySessionsResponse` | struct | [`UsbSecurityKeySessionsResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4025) | struct { `sessions`: `Vec` } | -| `UsbSecurityKeyCancelResponse` | struct | [`UsbSecurityKeyCancelResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4043) | struct { `cancelled_session_id`: `Option`; `already_idle`: `bool` } | -| `UsbSecurityKeyTestResponse` | struct | [`UsbSecurityKeyTestResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4080) | struct { `vm`: `String`; `ok`: `bool`; `checks`: `Vec` } | +| `PublicResponse` | enum | [`PublicResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L149) | `Capabilities` - (CapabilitiesResponse); `AuthStatus` - (AuthStatusResponse); `List` - (ListResponse); `Status` - (StatusResponse); `Audit` - (AuditResponse); `HostCheck` - (HostCheckResponse); `KeysList` - (KeysListResponse); `KeysShow` - (KeysShowResponse); `UsbipProbe` - (UsbipProbeResponse); `StoreVerify` - (StoreVerifyResponse); `MutatingVerb` - (MutatingVerbResponse); `Exec` - (ExecOpResponse); `Console` - (ConsoleOpResponse); `Audio` - (AudioOpResponse); `GatewayDisplay` - (GatewayDisplayOpResponse); `Workload` - (WorkloadOpResponse); `UsbSecurityKeyStatus` - (d2b_contracts::security_key::SecurityKeyStatusResponse); `UsbSecurityKeySessions` - (d2b_contracts::security_key::SecurityKeySessionsResponse); `UsbSecurityKeyCancel` - (d2b_contracts::security_key::SecurityKeyCancelResponse); `Error` - (Error) | +| `WorkloadOpResponse` | enum | [`WorkloadOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L210) | `List` - (WorkloadListResult); `Status` - (Box); `LauncherExec` - (LauncherExecResult) | +| `GatewayDisplayOpResponse` | enum | [`GatewayDisplayOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L365) | `Start` - (GatewayDisplayStartResult); `Stop` - (GatewayDisplayStopResult); `Open` - (GatewayDisplayOpenResult); `Close` - (GatewayDisplayCloseResult); `List` - (GatewayDisplayListResult); `ListDetailed` - (GatewayDisplayListDetailedResult) | +| `ExecOpResponse` | enum | [`ExecOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1223) | `Start` - (ExecStartResult); `DetachedCreate` - (ExecDetachedCreateResult); `WriteStdin` - (ExecWriteStdinResult); `ReadOutput` - (ExecReadOutputResult); `Signal` - (ExecControlResult); `Resize` - (ExecControlResult); `Wait` - (ExecWaitResult); `Close` - (ExecCloseResult); `List` - (ExecDetachedListResult); `Logs` - (ExecDetachedLogsResult); `Status` - (ExecDetachedStatusResult); `Kill` - (ExecDetachedKillResult) | +| `NamedProcessStreamResponse` | enum | [`NamedProcessStreamResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1350) | `Stdin` - (ExecWriteStdinResult); `Output` - (ExecReadOutputResult); `Delivered` - (ExecControlResult); `Wait` - (ExecWaitResult); `Closed` - (ExecCloseResult); `Terminal` - (ExecTerminalStatus); `Error` - (NamedProcessStreamError) | +| `ConsoleOpResponse` | enum | [`ConsoleOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2037) | `Attach` - (ConsoleAttachResult); `WriteStdin` - (ConsoleControlResult); `ReadOutput` - (ConsoleReadOutputResult); `Resize` - (ConsoleControlResult); `Wait` - (ConsoleWaitResult); `Close` - (ConsoleCloseResult) | +| `AudioOpResponse` | enum | [`AudioOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2244) | `Status` - (AudioStatusResult); `SetVolume` - (AudioSetResult); `Mute` - (AudioSetResult) | +| `MutatingVerbResponse` | struct | [`MutatingVerbResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2326) | struct { `verb`: `String`; `outcome`: `MutatingVerbOutcome`; `target_wave`: `Option`; `summary`: `Option`; `remediation`: `Option`; `api_ready`: `Option` } | +| `CapabilitiesResponse` | struct | [`CapabilitiesResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2352) | struct { `broker_socket`: `String`; `capabilities`: `Vec`; `public_socket`: `String`; `server_version`: `Version`; `selected_version`: `Version` } | +| `AuthStatusResponse` | struct | [`AuthStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2362) | struct { `allowed_subcommands`: `Vec`; `denied_subcommands`: `Vec`; `role`: `AuthRole`; `sockets`: `Vec` } | +| `ListResponse` | struct | [`ListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2371) | struct { `vms`: `Vec`; `read_model`: `Option` } | +| `StatusResponse` | struct | [`StatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2379) | struct { `entries`: `Vec`; `read_model`: `Option` } | +| `AuditResponse` | struct | [`AuditResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2399) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | +| `HostCheckResponse` | struct | [`HostCheckResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2449) | struct { `exit_code`: `u8`; `findings`: `Vec` } | +| `KeysListResponse` | struct | [`KeysListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2467) | struct { `entries`: `Vec` } | +| `KeysShowResponse` | struct | [`KeysShowResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2473) | struct { `vm`: `String`; `env`: `Option`; `managed_key_path`: `String`; `public_key`: `String`; `fingerprint`: `String`; `known_hosts_entry`: `Option` } | +| `UsbipProbeResponse` | struct | [`UsbipProbeResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2714) | struct { `entries`: `Vec` } | +| `UsbSecurityKeyStatusResponse` | struct | [`UsbSecurityKeyStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4127) | struct { `host_proxy_enabled`: `bool`; `physical_keys`: `Vec`; `vm_devices`: `Vec`; `lease`: `UsbSkLeaseStatus` } | +| `UsbSecurityKeySessionsResponse` | struct | [`UsbSecurityKeySessionsResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4189) | struct { `sessions`: `Vec` } | +| `UsbSecurityKeyCancelResponse` | struct | [`UsbSecurityKeyCancelResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4207) | struct { `cancelled_session_id`: `Option`; `already_idle`: `bool` } | +| `UsbSecurityKeyTestResponse` | struct | [`UsbSecurityKeyTestResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4244) | struct { `vm`: `String`; `ok`: `bool`; `checks`: `Vec` } | ### Broker socket response types | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `ApplyHostGenerationHandoffResponse` | struct | [`ApplyHostGenerationHandoffResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L332) | struct { `target`: `d2b_contracts_resource::v3::ResourceRef`; `state`: `crate::host_generation::HandoffState`; `source_generation`: `u64`; `target_generation`: `u64`; `source_remains_usable`: `bool`; `summary`: `String` } | -| `LaunchCutoverRunnerResponse` | struct | [`LaunchCutoverRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L359) | struct { `operation_id`: `BundleOpId`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `Option` } | -| `CutoverAuditResponse` | struct | [`CutoverAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L426) | struct { `record_id`: `CanonicalAuditDigest` } | -| `CutoverVerificationResponse` | struct | [`CutoverVerificationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L521) | struct { `zones`: `Vec`; `sources_preserved`: `bool`; `identity_digests_match`: `bool`; `candidate_current`: `bool` } | -| `CutoverAdmissionResponse` | struct | [`CutoverAdmissionResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L533) | struct { `candidate_current`: `bool`; `markers_valid`: `bool`; `ownership_valid`: `bool`; `predicates_hold`: `bool` } | -| `CutoverEffectResponse` | struct | [`CutoverEffectResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L634) | struct { `outcome`: `CutoverEffectOutcome`; `identity`: `Option`; `verification`: `Option`; `admission`: `Option`; `audit_record_id`: `CanonicalAuditDigest` } | -| `RunHostInstallResponse` | struct | [`RunHostInstallResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1296) | struct { `installed`: `bool`; `enabled`: `bool`; `started`: `bool`; `artifacts_written`: `Vec` } | -| `RunMigrateResponse` | struct | [`RunMigrateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1314) | struct { `migrated_vm_count`: `u32`; `notes`: `Vec` } | -| `RunActivationResponse` | struct | [`RunActivationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1354) | struct { `mode`: `ActivationMode`; `vm`: `String`; `generation_number`: `Option`; `guest_switch_script_path`: `Option`; `summary`: `String` } | -| `RunGcResponse` | struct | [`RunGcResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1376) | struct { `keep_generations`: `Option`; `retained_store_path_count`: `u32`; `summary`: `String` } | -| `RunKeysRotateResponse` | struct | [`RunKeysRotateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1394) | struct { `vm`: `String`; `key_path`: `String`; `public_key_fingerprint`: `String` } | -| `RunHostKeyTrustResponse` | struct | [`RunHostKeyTrustResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1411) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `updated`: `bool` } | -| `RunRotateKnownHostResponse` | struct | [`RunRotateKnownHostResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1429) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `removed`: `bool` } | -| `BrokerResponse` | enum | [`BrokerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1451) | `ApplyHostGenerationHandoff` - (ApplyHostGenerationHandoffResponse); `LaunchCutoverRunner` - (LaunchCutoverRunnerResponse); `CutoverAudit` - (CutoverAuditResponse); `CutoverEffect` - (CutoverEffectResponse); `Ack` - (AckResponse); `CreatePersistentTap` - (TapReadyResponse); `CreateTapFd` - (TapReadyResponse); `Error` - (BrokerErrorResponse); `ExportBrokerAudit` - (ExportBrokerAuditResponse); `RunHostInstall` - (RunHostInstallResponse); `RunMigrate` - (RunMigrateResponse); `RunActivation` - (RunActivationResponse); `RunGc` - (RunGcResponse); `RunKeysRotate` - (RunKeysRotateResponse); `RunHostKeyTrust` - (RunHostKeyTrustResponse); `RunRotateKnownHost` - (RunRotateKnownHostResponse); `Hello` - (HelloResponse); `GuestControlSign` - (GuestControlSignResponse); `QemuMediaEnroll` - (QemuMediaEnrollResponse); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryResponse); `QemuMediaBoot` - (QemuMediaHotplugResponse); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleResponse); `QemuMediaQueryStatus` - (QemuMediaQueryStatusResponse); `QemuMediaQuit` - (QemuMediaLifecycleResponse); `QemuMediaAttach` - (QemuMediaHotplugResponse); `QemuMediaDetach` - (QemuMediaHotplugResponse); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyResponse); `OpenPidfd` - (OpenPidfdResponse); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketResponse); `ObserveRunner` - (ObserveRunnerResponse); `PipeWireAudio` - (PipeWireAudioResponse); `StartSystemdUnit` - (StartSystemdUnitResponse); `CheckSystemdUserManager` - (CheckSystemdUserManagerResponse); `ObserveSystemdUnit` - (ObserveSystemdUnitResponse); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdResponse); `StopSystemdUnit` - (StopSystemdUnitResponse); `OpenZoneStore` - (OpenZoneStoreResponse); `ResourceActivationAudit` - (ResourceActivationAuditResponse); `PollChildReaped` - (PollChildReapedResponse); `ReconcileStorageScope` - (ReconcileStorageScopeResponse); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateResponse); `SetBridgePortFlags` - (BridgePortFlagsResponse); `SignalRunner` - (SignalRunnerResponse); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdResponse); `SpawnRunner` - (SpawnRunnerResponse); `StoreSync` - (StoreSyncResponse); `StoreVerify` - (StoreVerifyResponse); `ValidateLockSpec` - (ValidateLockSpecResponse); `ValidateBundle` - (ValidateBundleResponse) | -| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1555) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | -| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1582) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | -| `GuestControlSignResponse` | struct | [`GuestControlSignResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1930) | struct { `tag`: `Vec` } | -| `QemuMediaEnrollResponse` | struct | [`QemuMediaEnrollResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2015) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `read_only`: `bool`; `enrolled`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | -| `QemuMediaRefreshRegistryResponse` | struct | [`QemuMediaRefreshRegistryResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2033) | struct { `record_count`: `u32`; `redacted_index_written`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | -| `QemuMediaLifecycleResponse` | struct | [`QemuMediaLifecycleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2109) | struct { `vm_id`: `VmId`; `command`: `QemuMediaLifecycleAction` } | -| `QemuMediaQueryStatusResponse` | struct | [`QemuMediaQueryStatusResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2116) | struct { `vm_id`: `VmId`; `status`: `QemuMediaVmStatus` } | -| `QemuMediaHotplugResponse` | struct | [`QemuMediaHotplugResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2159) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `slot`: `String`; `read_only`: `bool`; `qmp_commands`: `Vec`; `events`: `Vec` } | -| `OpenPidfdResponse` | struct | [`OpenPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2214) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `verified_start_time_ticks`: `u64`; `pidfd_index`: `u32` } | -| `OpenPeerPidfdFromAcceptedSocketResponse` | struct | [`OpenPeerPidfdFromAcceptedSocketResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2235) | struct { `pidfd_index`: `u32` } | -| `ObserveRunnerResponse` | struct | [`ObserveRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2262) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `pid`: `i32`; `start_time_ticks`: `u64`; `cgroup_verified`: `bool`; `executable_verified`: `bool` } | -| `PipeWireAudioResponse` | struct | [`PipeWireAudioResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2314) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `applied`: `bool`; `host_ready`: `bool`; `node_present`: `bool` } | -| `StartTransientUnitResponse` | struct | [`StartTransientUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2449) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | -| `CheckSystemdUserManagerResponse` | struct | [`CheckSystemdUserManagerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2461) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `available`: `bool` } | -| `ObserveSystemdUnitResponse` | struct | [`ObserveSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2470) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `identity`: `Option` } | -| `OpenSystemdUnitPidfdResponse` | struct | [`OpenSystemdUnitPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2481) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | -| `StopSystemdUnitResponse` | struct | [`StopSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2491) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `stopped`: `bool` } | -| `OpenZoneStoreResponse` | struct | [`OpenZoneStoreResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2527) | struct { `zone_store_id`: `ZoneStoreId`; `store_identity`: `String`; `disposition`: `ZoneStoreDisposition`; `fd_index`: `u32` } | -| `OpenHidrawSecurityKeyResponse` | struct | [`OpenHidrawSecurityKeyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2586) | struct { `selector_resolved`: `String`; `device_class`: `String` } | -| `MigrateLegacySwtpmStateResponse` | struct | [`MigrateLegacySwtpmStateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2656) | struct { `outcome`: `LegacySwtpmMigrationOutcome` } | -| `StoreSyncResponse` | struct | [`StoreSyncResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2703) | struct { `vm`: `String`; `generation_id`: `String`; `generation_token`: `u32`; `hardlink_farm_path`: `String`; `closure_count`: `u32`; `retained_generations`: `Vec`; `swept_count`: `u32`; `cleanup_deferred`: `bool` } | -| `AckResponse` | struct | [`AckResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2857) | struct { `accepted`: `bool`; `operation`: `String` } | -| `TapReadyResponse` | struct | [`TapReadyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2864) | struct { `bridge`: `Option`; `tap`: `IfName` } | -| `ExportBrokerAuditResponse` | struct | [`ExportBrokerAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2871) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | -| `BridgePortFlagsResponse` | struct | [`BridgePortFlagsResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2916) | struct { `bridge`: `IfName`; `isolated`: `bool`; `neigh_suppress`: `bool`; `port`: `IfName` } | -| `ValidateBundleResponse` | struct | [`ValidateBundleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2925) | struct { `valid`: `bool` } | -| `ResourceActivationAuditResponse` | struct | [`ResourceActivationAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2947) | struct { `recorded`: `bool` } | -| `SignalRunnerResponse` | struct | [`SignalRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2984) | struct { `signaled`: `bool`; `vm_id`: `VmId`; `role_id`: `RoleId` } | -| `DeregisterRunnerPidfdResponse` | struct | [`DeregisterRunnerPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3020) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `removed`: `bool` } | -| `SpawnRunnerResponse` | struct | [`SpawnRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3222) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `bundle_content_identity`: `Option`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `u32`; `console_fd_index`: `Option` } | -| `ReconcileStorageScopeResponse` | struct | [`ReconcileStorageScopeResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3468) | struct { `storage_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `status`: `StorageReconcileStatus`; `applied`: `bool`; `path_hash`: `String` } | -| `ValidateLockSpecResponse` | struct | [`ValidateLockSpecResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3492) | struct { `lock_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `cloexec_required`: `bool`; `fd_passing_mechanism`: `String`; `order_key`: `String` } | -| `PollChildReapedResponse` | struct | [`PollChildReapedResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3579) | struct { `notifications`: `Vec` } | +| `ApplyHostGenerationHandoffResponse` | struct | [`ApplyHostGenerationHandoffResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L321) | struct { `target`: `d2b_contracts_resource::v3::ResourceRef`; `state`: `crate::host_generation::HandoffState`; `source_generation`: `u64`; `target_generation`: `u64`; `source_remains_usable`: `bool`; `summary`: `String` } | +| `RunHostInstallResponse` | struct | [`RunHostInstallResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1234) | struct { `installed`: `bool`; `enabled`: `bool`; `started`: `bool`; `artifacts_written`: `Vec` } | +| `RunMigrateResponse` | struct | [`RunMigrateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1252) | struct { `migrated_vm_count`: `u32`; `notes`: `Vec` } | +| `RunActivationResponse` | struct | [`RunActivationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1292) | struct { `mode`: `ActivationMode`; `vm`: `String`; `generation_number`: `Option`; `guest_switch_script_path`: `Option`; `summary`: `String` } | +| `RunGcResponse` | struct | [`RunGcResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1314) | struct { `keep_generations`: `Option`; `retained_store_path_count`: `u32`; `summary`: `String` } | +| `RunKeysRotateResponse` | struct | [`RunKeysRotateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1332) | struct { `vm`: `String`; `key_path`: `String`; `public_key_fingerprint`: `String` } | +| `RunHostKeyTrustResponse` | struct | [`RunHostKeyTrustResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1349) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `updated`: `bool` } | +| `RunRotateKnownHostResponse` | struct | [`RunRotateKnownHostResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1367) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `removed`: `bool` } | +| `BrokerResponse` | enum | [`BrokerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1389) | `ApplyHostGenerationHandoff` - (ApplyHostGenerationHandoffResponse); `Ack` - (AckResponse); `CreatePersistentTap` - (TapReadyResponse); `CreateTapFd` - (TapReadyResponse); `Error` - (BrokerErrorResponse); `ExportBrokerAudit` - (ExportBrokerAuditResponse); `RunHostInstall` - (RunHostInstallResponse); `RunMigrate` - (RunMigrateResponse); `RunActivation` - (RunActivationResponse); `RunGc` - (RunGcResponse); `RunKeysRotate` - (RunKeysRotateResponse); `RunHostKeyTrust` - (RunHostKeyTrustResponse); `RunRotateKnownHost` - (RunRotateKnownHostResponse); `Hello` - (HelloResponse); `QemuMediaEnroll` - (QemuMediaEnrollResponse); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryResponse); `QemuMediaBoot` - (QemuMediaHotplugResponse); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleResponse); `QemuMediaQueryStatus` - (QemuMediaQueryStatusResponse); `QemuMediaQuit` - (QemuMediaLifecycleResponse); `QemuMediaAttach` - (QemuMediaHotplugResponse); `QemuMediaDetach` - (QemuMediaHotplugResponse); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyResponse); `OpenPidfd` - (OpenPidfdResponse); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketResponse); `ObserveRunner` - (ObserveRunnerResponse); `PipeWireAudio` - (PipeWireAudioResponse); `StartSystemdUnit` - (StartSystemdUnitResponse); `CheckSystemdUserManager` - (CheckSystemdUserManagerResponse); `ObserveSystemdUnit` - (ObserveSystemdUnitResponse); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdResponse); `StopSystemdUnit` - (StopSystemdUnitResponse); `OpenZoneStore` - (OpenZoneStoreResponse); `ResourceActivationAudit` - (ResourceActivationAuditResponse); `PollChildReaped` - (PollChildReapedResponse); `ReconcileStorageScope` - (ReconcileStorageScopeResponse); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateResponse); `SetBridgePortFlags` - (BridgePortFlagsResponse); `SignalRunner` - (SignalRunnerResponse); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdResponse); `SpawnRunner` - (SpawnRunnerResponse); `StoreSync` - (StoreSyncResponse); `StoreVerify` - (StoreVerifyResponse); `ValidateLockSpec` - (ValidateLockSpecResponse); `ValidateBundle` - (ValidateBundleResponse) | +| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1486) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | +| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1513) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | +| `QemuMediaEnrollResponse` | struct | [`QemuMediaEnrollResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1838) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `read_only`: `bool`; `enrolled`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | +| `QemuMediaRefreshRegistryResponse` | struct | [`QemuMediaRefreshRegistryResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1856) | struct { `record_count`: `u32`; `redacted_index_written`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | +| `QemuMediaLifecycleResponse` | struct | [`QemuMediaLifecycleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1932) | struct { `vm_id`: `VmId`; `command`: `QemuMediaLifecycleAction` } | +| `QemuMediaQueryStatusResponse` | struct | [`QemuMediaQueryStatusResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1939) | struct { `vm_id`: `VmId`; `status`: `QemuMediaVmStatus` } | +| `QemuMediaHotplugResponse` | struct | [`QemuMediaHotplugResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1982) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `slot`: `String`; `read_only`: `bool`; `qmp_commands`: `Vec`; `events`: `Vec` } | +| `OpenPidfdResponse` | struct | [`OpenPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2069) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `verified_start_time_ticks`: `u64`; `pidfd_index`: `u32` } | +| `OpenPeerPidfdFromAcceptedSocketResponse` | struct | [`OpenPeerPidfdFromAcceptedSocketResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2090) | struct { `pidfd_index`: `u32` } | +| `ObserveRunnerResponse` | struct | [`ObserveRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2120) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `pid`: `i32`; `start_time_ticks`: `u64`; `cgroup_verified`: `bool`; `executable_verified`: `bool` } | +| `PipeWireAudioResponse` | struct | [`PipeWireAudioResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2172) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `applied`: `bool`; `host_ready`: `bool`; `node_present`: `bool` } | +| `StartTransientUnitResponse` | struct | [`StartTransientUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2313) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | +| `CheckSystemdUserManagerResponse` | struct | [`CheckSystemdUserManagerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2325) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `available`: `bool` } | +| `ObserveSystemdUnitResponse` | struct | [`ObserveSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2334) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `identity`: `Option` } | +| `OpenSystemdUnitPidfdResponse` | struct | [`OpenSystemdUnitPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2345) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | +| `StopSystemdUnitResponse` | struct | [`StopSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2355) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `stopped`: `bool` } | +| `OpenZoneStoreResponse` | struct | [`OpenZoneStoreResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2391) | struct { `zone_store_id`: `ZoneStoreId`; `store_identity`: `String`; `disposition`: `ZoneStoreDisposition`; `fd_index`: `u32` } | +| `OpenHidrawSecurityKeyResponse` | struct | [`OpenHidrawSecurityKeyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2450) | struct { `selector_resolved`: `String`; `device_class`: `String` } | +| `MigrateLegacySwtpmStateResponse` | struct | [`MigrateLegacySwtpmStateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2520) | struct { `outcome`: `LegacySwtpmMigrationOutcome` } | +| `StoreSyncResponse` | struct | [`StoreSyncResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2567) | struct { `vm`: `String`; `generation_id`: `String`; `generation_token`: `u32`; `hardlink_farm_path`: `String`; `closure_count`: `u32`; `retained_generations`: `Vec`; `swept_count`: `u32`; `cleanup_deferred`: `bool` } | +| `AckResponse` | struct | [`AckResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2721) | struct { `accepted`: `bool`; `operation`: `String` } | +| `TapReadyResponse` | struct | [`TapReadyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2728) | struct { `bridge`: `Option`; `tap`: `IfName` } | +| `ExportBrokerAuditResponse` | struct | [`ExportBrokerAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2735) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | +| `BridgePortFlagsResponse` | struct | [`BridgePortFlagsResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2780) | struct { `bridge`: `IfName`; `isolated`: `bool`; `neigh_suppress`: `bool`; `port`: `IfName` } | +| `ValidateBundleResponse` | struct | [`ValidateBundleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2789) | struct { `valid`: `bool` } | +| `ResourceActivationAuditResponse` | struct | [`ResourceActivationAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2811) | struct { `recorded`: `bool` } | +| `SignalRunnerResponse` | struct | [`SignalRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2851) | struct { `signaled`: `bool`; `vm_id`: `VmId`; `role_id`: `RoleId` } | +| `DeregisterRunnerPidfdResponse` | struct | [`DeregisterRunnerPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2890) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `removed`: `bool` } | +| `SpawnRunnerResponse` | struct | [`SpawnRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3110) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `guest_execution`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `bundle_content_identity`: `Option`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `u32`; `console_fd_index`: `Option` } | +| `ReconcileStorageScopeResponse` | struct | [`ReconcileStorageScopeResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3347) | struct { `storage_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `status`: `StorageReconcileStatus`; `applied`: `bool`; `path_hash`: `String` } | +| `ValidateLockSpecResponse` | struct | [`ValidateLockSpecResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3371) | struct { `lock_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `cloexec_required`: `bool`; `fd_passing_mechanism`: `String`; `order_key`: `String` } | +| `PollChildReapedResponse` | struct | [`PollChildReapedResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3458) | struct { `notifications`: `Vec` } | ## Per-VM lifecycle state @@ -522,15 +506,16 @@ hand. Live activation (`Switch`, `Test`, and live `Rollback`) is not a broker script-execution surface. `d2bd` prepares the VM toplevel, asks the broker/store-view path to publish the closure into the per-VM live store -pool, opens authenticated guest-control to guestd, starts activation of -the prepared toplevel inside the running guest, polls guest activation -status, and only then asks the broker to commit host-owned generation -metadata. If the VM is stopped/offline or guestd does not advertise the -activation capability, the operation fails closed before host-side +pool, creates the target-local activation Process through the Resource API, +starts activation of the prepared toplevel inside the running guest, polls +guest activation status, and only then asks the broker to commit host-owned +generation metadata. If the VM is stopped/offline or the activation Process +is unavailable, the operation fails closed before host-side generation commit. `Boot` is the explicit offline staging mode. It publishes and commits -the declared toplevel for the next VM start without contacting guestd or +the declared toplevel for the next VM start without contacting the +target-local activation Process or running live guest activation. @@ -538,111 +523,73 @@ running live guest activation. | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `VmLifecycleState` | enum | [`VmLifecycleState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2789) | `Stopped`; `Starting`; `Booted`; `Running`; `Stopping`; `Restarting`; `Failed`; `Unknown` | +| `VmLifecycleState` | enum | [`VmLifecycleState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2874) | `Stopped`; `Starting`; `Booted`; `Running`; `Stopping`; `Restarting`; `Failed`; `Unknown` | ### Other documented enums | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `CutoverAuditTransition` | enum | [`CutoverAuditTransition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L374) | `HoldRequested`; `HoldCleared`; `PhaseStarted`; `PhaseCompleted`; `EffectStarted`; `EffectCompleted`; `Terminal` | -| `CutoverEffectAuthority` | enum | [`CutoverEffectAuthority`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L434) | `Cutover`; `ResetZone`; `ResetProvider`; `ResetGuest` | -| `CutoverEffectKind` | enum | [`CutoverEffectKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L448) | `HostDrain`; `CutoverDisposition`; `ResourceStoreCreate`; `ProviderInstall`; `ZoneActivation`; `GuestActivation`; `Verification`; `CutoverFinalization`; `ScopedZoneReset`; `ScopedProviderReset`; `ScopedGuestReset`; `DestroyDurableVolume`; `PreserveSource`; `QuarantineDestination`; `CutoverBroker`; `ClosureActivation`; `ApplyAdmission` | -| `CutoverEffectPayload` | enum | [`CutoverEffectPayload`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L474) | `None`; `ApplyAdmission` - (CutoverAdmissionRequest); `Storage` - (ReconcileStorageScopeRequest); `ZoneStore` - (OpenZoneStoreRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `Verification` - (CutoverVerificationRequest); `Activation` - (RunActivationRequest); `Systemd` - (Box); `Quarantine` - struct { `staged_id`: `BundleOpId`; `source_id`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest` }; `Finalization` - struct { `artifacts`: `Vec`; `disposition_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` }; `DestroyDurableVolume` - struct { `storage_ref`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` } | -| `CutoverReplayClass` | enum | [`CutoverReplayClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L553) | `Repeatable`; `ReopenByJournaledIdentity`; `QuarantineOnly` | -| `CutoverEffectOutcome` | enum | [`CutoverEffectOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L625) | `Succeeded`; `Failed`; `Ambiguous` | -| `ActivationMode` | enum | [`ActivationMode`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1321) | `Switch`; `Boot`; `Test`; `Rollback` | -| `ActivationPhase` | enum | [`ActivationPhase`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1330) | `Prepare`; `Commit`; `MetadataOnly` | -| `NftablesProjectionAction` | enum | [`NftablesProjectionAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1613) | `Apply`; `Remove` | -| `BrokerAuditSeverity` | enum | [`BrokerAuditSeverity`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1816) | `Info`; `Warning`; `Error`; `Denied` | -| `GuestControlProofRole` | enum | [`GuestControlProofRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1829) | `HostProof`; `GuestProof` | -| `GuestControlDirection` | enum | [`GuestControlDirection`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1836) | `HostToGuest` | -| `GuestControlAuthPurpose` | enum | [`GuestControlAuthPurpose`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1842) | `GuestControlAuthV1` | -| `QemuMediaLifecycleAction` | enum | [`QemuMediaLifecycleAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2078) | `SystemPowerdown`; `Quit` | -| `QemuMediaVmStatus` | enum | [`QemuMediaVmStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2085) | `Running`; `Paused`; `Shutdown`; `Suspended`; `Watchdog`; `Debug`; `Inmigrate`; `InternalError`; `IoError`; `Postmigrate`; `Prelaunch`; `FinishMigrate`; `RestoreVm`; `SaveVm`; `GuestPanicked`; `Colo`; `Preconfig`; `Unknown`; `ConnectionLostDuringShutdown` | -| `QemuMediaHotplugStatus` | enum | [`QemuMediaHotplugStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2138) | `IdentityResolved`; `QmpConnected`; `QmpCapabilities`; `FdAdded`; `BlockdevAdded`; `DeviceAdded`; `DeviceDeleted`; `BlockdevDeleted`; `FdRemoved`; `VmContinued` | -| `PipeWireAudioChannel` | enum | [`PipeWireAudioChannel`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2275) | `Speaker`; `Microphone` | -| `PipeWireAudioAction` | enum | [`PipeWireAudioAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2285) | `SetGrant` - struct { `on`: `bool` }; `SetLevel` - struct { `percent`: `u8` } | -| `SystemdUnitDomain` | enum | [`SystemdUnitDomain`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2329) | `System`; `User` | -| `SystemdStopClass` | enum | [`SystemdStopClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2439) | `Drain`; `Terminate` | -| `ZoneStoreDisposition` | enum | [`ZoneStoreDisposition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2509) | `Provisioned`; `Opened` | -| `LegacySwtpmMigrationOutcome` | enum | [`LegacySwtpmMigrationOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2624) | `Migrated`; `AlreadyMigrated`; `NotApplicable`; `Pending`; `Failed`; `Ambiguous`; `AdoptionRequired`; `NeverProvisioned` | -| `RunnerSignal` | enum | [`RunnerSignal`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2957) | `Term`; `Kill`; `Quit` | -| `RunnerRole` | enum | [`RunnerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3039) | `CloudHypervisor`; `QemuMedia`; `Virtiofsd`; `Swtpm`; `SwtpmFlush`; `Gpu`; `Audio`; `Video`; `VsockRelay`; `Usbip`; `OtelHostBridge`; `WaylandProxy` | -| `RunnerAllocationKind` | enum | [`RunnerAllocationKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3200) | `VsockCid`; `TapFdSlot`; `ApiSocketPath` | -| `BrokerCallerRole` | enum | [`BrokerCallerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3342) | `AdminUid` - struct { `uid`: `u32` }; `LauncherUid` - struct { `uid`: `u32` }; `RootUid` - struct { `uid`: `u32` }; `CutoverRunner` - struct { `operation_id`: `BundleOpId`; `capability_digest`: `CanonicalAuditDigest` }; `NotAuthorized` | -| `StorageReconcileStatus` | enum | [`StorageReconcileStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3457) | `Clean`; `Created`; `Reused`; `CheckedOnly`; `TemplateUnexpanded`; `Refused` | -| `ChildExitKind` | enum | [`ChildExitKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3531) | `Exited`; `Signaled`; `Killed` | -| `BrokerNotification` | enum | [`BrokerNotification`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3569) | `ChildReaped` - (ChildReapedNotification); `Unknown` | +| `BrokerProfile` | enum | [`BrokerProfile`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L950) | `Host`; `Guest` | +| `ActivationMode` | enum | [`ActivationMode`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1259) | `Switch`; `Boot`; `Test`; `Rollback` | +| `ActivationPhase` | enum | [`ActivationPhase`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1268) | `Prepare`; `Commit`; `MetadataOnly` | +| `NftablesProjectionAction` | enum | [`NftablesProjectionAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1544) | `Apply`; `Remove` | +| `BrokerAuditSeverity` | enum | [`BrokerAuditSeverity`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1747) | `Info`; `Warning`; `Error`; `Denied` | +| `QemuMediaLifecycleAction` | enum | [`QemuMediaLifecycleAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1901) | `SystemPowerdown`; `Quit` | +| `QemuMediaVmStatus` | enum | [`QemuMediaVmStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1908) | `Running`; `Paused`; `Shutdown`; `Suspended`; `Watchdog`; `Debug`; `Inmigrate`; `InternalError`; `IoError`; `Postmigrate`; `Prelaunch`; `FinishMigrate`; `RestoreVm`; `SaveVm`; `GuestPanicked`; `Colo`; `Preconfig`; `Unknown`; `ConnectionLostDuringShutdown` | +| `QemuMediaHotplugStatus` | enum | [`QemuMediaHotplugStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1961) | `IdentityResolved`; `QmpConnected`; `QmpCapabilities`; `FdAdded`; `BlockdevAdded`; `DeviceAdded`; `DeviceDeleted`; `BlockdevDeleted`; `FdRemoved`; `VmContinued` | +| `PipeWireAudioChannel` | enum | [`PipeWireAudioChannel`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2133) | `Speaker`; `Microphone` | +| `PipeWireAudioAction` | enum | [`PipeWireAudioAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2143) | `SetGrant` - struct { `on`: `bool` }; `SetLevel` - struct { `percent`: `u8` } | +| `SystemdUnitDomain` | enum | [`SystemdUnitDomain`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2187) | `System`; `User` | +| `SystemdStopClass` | enum | [`SystemdStopClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2303) | `Drain`; `Terminate` | +| `ZoneStoreDisposition` | enum | [`ZoneStoreDisposition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2373) | `Provisioned`; `Opened` | +| `LegacySwtpmMigrationOutcome` | enum | [`LegacySwtpmMigrationOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2488) | `Migrated`; `AlreadyMigrated`; `NotApplicable`; `Pending`; `Failed`; `Ambiguous`; `AdoptionRequired`; `NeverProvisioned` | +| `RunnerSignal` | enum | [`RunnerSignal`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2821) | `Term`; `Kill`; `Quit` | +| `RunnerRole` | enum | [`RunnerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2909) | `CloudHypervisor`; `QemuMedia`; `ActivationNixos`; `Virtiofsd`; `Swtpm`; `SwtpmFlush`; `Gpu`; `Audio`; `Video`; `VsockRelay`; `Usbip`; `OtelHostBridge`; `WaylandProxy` | +| `RunnerAllocationKind` | enum | [`RunnerAllocationKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3088) | `VsockCid`; `TapFdSlot`; `ApiSocketPath` | +| `BrokerCallerRole` | enum | [`BrokerCallerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3232) | `AdminUid` - struct { `uid`: `u32` }; `LauncherUid` - struct { `uid`: `u32` }; `RootUid` - struct { `uid`: `u32` }; `NotAuthorized` | +| `StorageReconcileStatus` | enum | [`StorageReconcileStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3336) | `Clean`; `Created`; `Reused`; `CheckedOnly`; `TemplateUnexpanded`; `Refused` | +| `ChildExitKind` | enum | [`ChildExitKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3410) | `Exited`; `Signaled`; `Killed` | +| `BrokerNotification` | enum | [`BrokerNotification`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3448) | `ChildReaped` - (ChildReapedNotification); `Unknown` | | `HandoffCallerRole` | enum | [`HandoffCallerRole`](../../packages/d2b-contracts-broker/src/host_generation.rs#L131) | `Lifecycle`; `Admin` | | `HandoffState` | enum | [`HandoffState`](../../packages/d2b-contracts-broker/src/host_generation.rs#L175) | `Recorded`; `Validated`; `Mutating`; `Transferred`; `Completed`; `RolledBack`; `Refused` | -| `AuthDirection` | enum | [`AuthDirection`](../../packages/d2b-contracts-control/src/guest_auth.rs#L13) | `HostToGuest` | -| `AuthPurpose` | enum | [`AuthPurpose`](../../packages/d2b-contracts-control/src/guest_auth.rs#L26) | `GuestControlAuthV1` | -| `ProofRole` | enum | [`ProofRole`](../../packages/d2b-contracts-control/src/guest_auth.rs#L39) | `Host`; `Guest` | -| `GuestCapability` | enum | [`GuestCapability`](../../packages/d2b-contracts-control/src/guest_wire.rs#L351) | `Health`; `Capabilities`; `ExecAttached`; `ExecDetached`; `ExecTty`; `ExecLogs`; `TtyResize`; `Signals`; `ReadGuestFile`; `UsbipImport`; `ShellAttached`; `ShellManagement`; `ShellForceAttach`; `UsbipStatus`; `SystemActivation`; `AudioStatus`; `AudioSet` | -| `GuestFileId` | enum | [`GuestFileId`](../../packages/d2b-contracts-control/src/guest_wire.rs#L379) | `GuestConfig` | -| `GuestSubsystem` | enum | [`GuestSubsystem`](../../packages/d2b-contracts-control/src/guest_wire.rs#L385) | `Guestd`; `Userd`; `Exec`; `LogStorage`; `Token`; `Vsock`; `Usbip`; `Shell`; `Shpool`; `SystemActivation`; `Audio` | -| `TerminalKind` | enum | [`TerminalKind`](../../packages/d2b-contracts-control/src/guest_wire.rs#L401) | `Exec`; `Shell` | -| `ShellState` | enum | [`ShellState`](../../packages/d2b-contracts-control/src/guest_wire.rs#L408) | `Attached`; `Detached`; `Killed`; `PoolUnavailable`; `FeatureDisabled`; `OutputGap` | -| `ShellCloseCause` | enum | [`ShellCloseCause`](../../packages/d2b-contracts-control/src/guest_wire.rs#L419) | `ClientDetach`; `EvictedByForce`; `EvictedByAdminDetach`; `KilledByAdmin`; `PoolUnavailable`; `OutputGap` | -| `HealthOrigin` | enum | [`HealthOrigin`](../../packages/d2b-contracts-control/src/guest_wire.rs#L430) | `GuestReported`; `HostSynthesized` | -| `GuestVsockDirection` | enum | [`GuestVsockDirection`](../../packages/d2b-contracts-control/src/guest_wire.rs#L437) | `HostToGuest` | -| `GuestIdentityBinding` | enum | [`GuestIdentityBinding`](../../packages/d2b-contracts-control/src/guest_wire.rs#L443) | `VmIdCidPortAndTokenTranscript` | -| `GuestTransportKind` | enum | [`GuestTransportKind`](../../packages/d2b-contracts-control/src/guest_wire.rs#L528) | `VirtioVsockTtrpc` | -| `GuestConnectAckValue` | enum | [`GuestConnectAckValue`](../../packages/d2b-contracts-control/src/guest_wire.rs#L542) | `OpaqueLocalPort` | -| `HealthState` | enum | [`HealthState`](../../packages/d2b-contracts-control/src/guest_wire.rs#L653) | `Healthy`; `Degraded`; `UnavailableOldGeneration`; `ListenerAbsent`; `TransportUnreachable`; `AuthFailed`; `ProtocolMismatch`; `StaleSession` | -| `HealthReason` | enum | [`HealthReason`](../../packages/d2b-contracts-control/src/guest_wire.rs#L666) | `None`; `OldGeneration`; `ListenerAbsent`; `ConnectRefused`; `ConnectTimeout`; `EofBeforeAck`; `MalformedAck`; `AckTooLong`; `TransportIo`; `AuthTokenRejected`; `ProtocolVersionUnsupported`; `SessionGenerationMismatch`; `ExecSubsystemUnavailable`; `LogStorageUnavailable`; `QuotaExceeded`; `RateLimited`; `InternalHealthCheckFailed` | -| `HealthRemediation` | enum | [`HealthRemediation`](../../packages/d2b-contracts-control/src/guest_wire.rs#L688) | `None`; `Retry`; `RestartVm`; `UpgradeGuest`; `CheckAuthToken`; `CheckGuestdService`; `ReduceLoad`; `InspectGuestLogs` | -| `GuestActivationMode` | enum | [`GuestActivationMode`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1075) | `Switch`; `Boot`; `Test`; `DryActivate` | -| `GuestActivationState` | enum | [`GuestActivationState`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1084) | `Running`; `Succeeded`; `Failed`; `TimedOut`; `Lost` | -| `UsbipImportAction` | enum | [`UsbipImportAction`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1278) | `Attach`; `Detach` | -| `AudioSetKind` | enum | [`AudioSetKind`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1370) | `Grant`; `Level` | -| `AudioChannel` | enum | [`AudioChannel`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1380) | `Microphone`; `Speaker` | -| `OutputStream` | enum | [`OutputStream`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1410) | `Stdout`; `Stderr` | -| `WriteDisposition` | enum | [`WriteDisposition`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1417) | `Accepted`; `Duplicate`; `Rejected` | -| `ExecState` | enum | [`ExecState`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1425) | `Created`; `Running`; `Exited`; `Signaled`; `Cancelled`; `SlowConsumerCancelled`; `ProtocolError`; `LostGuestd`; `Reaped` | -| `StdinState` | enum | [`StdinState`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1439) | `Open`; `Closing`; `Closed`; `ClosedByProcess`; `RejectedNotInteractive` | -| `TerminalStatus` | enum | [`TerminalStatus`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1454) | `ExitCode` - struct { `exit_code`: `i32` }; `Signal` - struct { `signal`: `u32` }; `StatusCode` - struct { `status_code`: `i32` }; `Error` - struct { `error`: `GuestControlErrorKind` } | -| `SignalTarget` | enum | [`SignalTarget`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1473) | `ForegroundProcessGroup`; `ProcessTree` | -| `ExecCancelReason` | enum | [`ExecCancelReason`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1480) | `ClientDisconnect`; `UserRequested`; `SlowConsumer`; `ProtocolError` | | `ProxyReadinessStage` | enum | [`ProxyReadinessStage`](../../packages/d2b-contracts-control/src/proxy_readiness.rs#L15) | `Upstream`; `Listener`; `FirstClient` | | `ProxyReadinessState` | enum | [`ProxyReadinessState`](../../packages/d2b-contracts-control/src/proxy_readiness.rs#L27) | `Ready`; `Failed` | | `ProxyReadinessFailure` | enum | [`ProxyReadinessFailure`](../../packages/d2b-contracts-control/src/proxy_readiness.rs#L37) | `UpstreamUnavailable`; `ListenerUnavailable`; `FirstClientTimeout`; `ClientRejected`; `ChannelUnavailable` | -| `WorkloadOp` | enum | [`WorkloadOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L207) | `List` - (WorkloadListArgs); `Status` - (WorkloadStatusArgs); `LauncherExec` - (LauncherExecArgs) | -| `WorkloadAvailability` | enum | [`WorkloadAvailability`](../../packages/d2b-contracts-control/src/public_wire.rs#L247) | `Ready`; `HelperUnavailable`; `HelperStale`; `UserManagerUnavailable`; `GraphicalSessionInactive`; `WaylandUnavailable`; `ProxyUnavailable`; `Degraded` | -| `GraphicalLaunchPosture` | enum | [`GraphicalLaunchPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L261) | `Proxied`; `NotApplicable`; `GraphicalSessionInactive`; `WaylandUnavailable`; `ProxyUnavailable` | -| `LauncherExecDisposition` | enum | [`LauncherExecDisposition`](../../packages/d2b-contracts-control/src/public_wire.rs#L302) | `Committed`; `AlreadyCommitted` | -| `GatewayDisplayOp` | enum | [`GatewayDisplayOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L318) | `Start` - (GatewayDisplayStartArgs); `Stop` - (GatewayDisplayStopArgs); `Open` - (GatewayDisplayOpenArgs); `Close` - (GatewayDisplayCloseArgs); `List` - (GatewayDisplayListArgs); `ListDetailed` - (GatewayDisplayListArgs) | -| `ExecStream` | enum | [`ExecStream`](../../packages/d2b-contracts-control/src/public_wire.rs#L620) | `Stdout`; `Stderr` | -| `ExecOp` | enum | [`ExecOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L911) | `Start` - (ExecStartArgs); `WriteStdin` - (ExecWriteStdinArgs); `ReadOutput` - (ExecReadOutputArgs); `Signal` - (ExecSignalArgs); `Resize` - (ExecResizeArgs); `Wait` - (ExecWaitArgs); `Close` - (ExecCloseArgs); `List` - (ExecDetachedListArgs); `Logs` - (ExecDetachedLogsArgs); `Status` - (ExecDetachedStatusArgs); `Kill` - (ExecDetachedKillArgs) | -| `ExecTerminalStatus` | enum | [`ExecTerminalStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L1027) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | -| `ExecDetachedKillOutcome` | enum | [`ExecDetachedKillOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L1218) | `Cancelling`; `AlreadyTerminal` | -| `ShellSessionState` | enum | [`ShellSessionState`](../../packages/d2b-contracts-control/src/public_wire.rs#L1412) | `Attached`; `Detached`; `Killed`; `PoolUnavailable`; `FeatureDisabled`; `OutputGap` | -| `ShellCloseCause` | enum | [`ShellCloseCause`](../../packages/d2b-contracts-control/src/public_wire.rs#L1423) | `ClientDetach`; `EvictedByForce`; `EvictedByAdminDetach`; `KilledByAdmin`; `PoolUnavailable`; `OutputGap` | -| `ConsoleProviderKind` | enum | [`ConsoleProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1533) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | -| `ConsoleOp` | enum | [`ConsoleOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1676) | `Attach` - (ConsoleAttachArgs); `WriteStdin` - (ConsoleWriteStdinArgs); `ReadOutput` - (ConsoleReadOutputArgs); `Resize` - (ConsoleResizeArgs); `Wait` - (ConsoleWaitArgs); `Close` - (ConsoleCloseArgs) | -| `AudioChannel` | enum | [`AudioChannel`](../../packages/d2b-contracts-control/src/public_wire.rs#L1822) | `Speaker`; `Microphone` | -| `AudioEnforcementPosture` | enum | [`AudioEnforcementPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L1836) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | -| `AudioProviderKind` | enum | [`AudioProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1878) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | -| `AudioOp` | enum | [`AudioOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1923) | `Status` - (AudioStatusArgs); `SetVolume` - (AudioSetVolumeArgs); `Mute` - (AudioMuteArgs) | -| `AudioSetApplied` | enum | [`AudioSetApplied`](../../packages/d2b-contracts-control/src/public_wire.rs#L1986) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | -| `HostCutoverOperation` | enum | [`HostCutoverOperation`](../../packages/d2b-contracts-control/src/public_wire.rs#L2142) | `Preview`; `Status`; `Hold`; `Resume`; `Apply`; `Rollback`; `Verify`; `Doctor`; `Finalize`; `Reset` | -| `HostCutoverResetScope` | enum | [`HostCutoverResetScope`](../../packages/d2b-contracts-control/src/public_wire.rs#L2168) | `Zone`; `Provider`; `Guest` | -| `MutatingVerbOutcome` | enum | [`MutatingVerbOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L2256) | `DryRunPlanned`; `Applied`; `ApiReadyTimeout`; `NotYetImplemented`; `BrokerError`; `InvalidRequest` | -| `UsbipProbeStatus` | enum | [`UsbipProbeStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L2400) | `Bound`; `Unbound`; `Degraded`; `Enrollable`; `Enrolled`; `Stale`; `DirectConfig`; `Unknown` | -| `UsbipDurableClaimState` | enum | [`UsbipDurableClaimState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2418) | `Missing`; `HeldByDesiredOwner`; `HeldByOtherOwner`; `StaleOwner`; `Corrupt`; `NotApplicable`; `Unknown` | -| `UsbipHostBindState` | enum | [`UsbipHostBindState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2443) | `Unbound`; `BoundToUsbipHost`; `BoundToUnexpectedDriver`; `DeviceMissing`; `NotApplicable`; `Unknown` | -| `UsbipHostCarrierState` | enum | [`UsbipHostCarrierState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2456) | `Absent`; `Unavailable`; `WithheldForOwner`; `Ready`; `DepartedDuringProbe`; `NotApplicable`; `Unknown` | -| `UsbipProxyState` | enum | [`UsbipProxyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2470) | `NotDeclared`; `Stopped`; `Starting`; `Listening`; `Stale`; `Failed`; `NotApplicable`; `Unknown` | -| `UsbipGuestImportState` | enum | [`UsbipGuestImportState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2493) | `Detached`; `Imported`; `Unavailable`; `NotApplicable`; `Unknown` | -| `UsbipTopologyState` | enum | [`UsbipTopologyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2511) | `Match`; `Mismatch`; `Incomplete`; `NotObserved`; `NotApplicable`; `Unknown` | -| `UsbipPolicyState` | enum | [`UsbipPolicyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2524) | `Allowed`; `Denied`; `Missing`; `NotApplicable`; `Unknown` | -| `UsbipProbeDegradedReasonCode` | enum | [`UsbipProbeDegradedReasonCode`](../../packages/d2b-contracts-control/src/public_wire.rs#L2543) | `PolicyFailed`; `DeviceDepartedBeforeClaim`; `DeviceDepartedAfterLock`; `DeviceDepartedDuringMutation`; `DeviceReappearedWithDifferentTopology`; `LockHeldByOtherOwner`; `InvalidPersistedLockClaim`; `CarrierUnavailable`; `HostBindUnavailable`; `ProxyUnavailable`; `GuestImportUnavailable`; `StaleHostState`; `StaleGuestState`; `ProbeIncomplete`; `Unknown` | -| `UsbProbeEntryKind` | enum | [`UsbProbeEntryKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2579) | `Usbip`; `QemuMediaSlot` | -| `AuditFormat` | enum | [`AuditFormat`](../../packages/d2b-contracts-control/src/public_wire.rs#L2649) | `Human`; `Json` | -| `AuthRole` | enum | [`AuthRole`](../../packages/d2b-contracts-control/src/public_wire.rs#L2657) | `None`; `Launcher`; `Admin` | -| `HostFindingSeverity` | enum | [`HostFindingSeverity`](../../packages/d2b-contracts-control/src/public_wire.rs#L2878) | `Pass`; `Warn`; `Fail` | -| `UsbSkLeaseState` | enum | [`UsbSkLeaseState`](../../packages/d2b-contracts-control/src/public_wire.rs#L3930) | `Idle`; `Active`; `Queued`; `Unknown` | -| `UsbSkSessionOutcome` | enum | [`UsbSkSessionOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L3985) | `Success`; `Timeout`; `Cancelled`; `DeviceUnavailable`; `Active`; `Unknown` | +| `ExecState` | enum | [`ExecState`](../../packages/d2b-contracts-control/src/public_wire.rs#L29) | `Created`; `Running`; `Exited`; `Signaled`; `Cancelled`; `SlowConsumerCancelled`; `ProtocolError`; `LostTarget`; `Reaped` | +| `WorkloadOp` | enum | [`WorkloadOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L202) | `List` - (WorkloadListArgs); `Status` - (WorkloadStatusArgs); `LauncherExec` - (LauncherExecArgs) | +| `WorkloadAvailability` | enum | [`WorkloadAvailability`](../../packages/d2b-contracts-control/src/public_wire.rs#L242) | `Ready`; `HelperUnavailable`; `HelperStale`; `UserManagerUnavailable`; `GraphicalSessionInactive`; `WaylandUnavailable`; `ProxyUnavailable`; `Degraded` | +| `GraphicalLaunchPosture` | enum | [`GraphicalLaunchPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L256) | `Proxied`; `NotApplicable`; `GraphicalSessionInactive`; `WaylandUnavailable`; `ProxyUnavailable` | +| `LauncherExecDisposition` | enum | [`LauncherExecDisposition`](../../packages/d2b-contracts-control/src/public_wire.rs#L297) | `Committed`; `AlreadyCommitted` | +| `GatewayDisplayOp` | enum | [`GatewayDisplayOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L313) | `Start` - (GatewayDisplayStartArgs); `Stop` - (GatewayDisplayStopArgs); `Open` - (GatewayDisplayOpenArgs); `Close` - (GatewayDisplayCloseArgs); `List` - (GatewayDisplayListArgs); `ListDetailed` - (GatewayDisplayListArgs) | +| `ExecStream` | enum | [`ExecStream`](../../packages/d2b-contracts-control/src/public_wire.rs#L597) | `Stdout`; `Stderr` | +| `ExecOp` | enum | [`ExecOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L888) | `Start` - (ExecStartArgs); `WriteStdin` - (ExecWriteStdinArgs); `ReadOutput` - (ExecReadOutputArgs); `Signal` - (ExecSignalArgs); `Resize` - (ExecResizeArgs); `Wait` - (ExecWaitArgs); `Close` - (ExecCloseArgs); `List` - (ExecDetachedListArgs); `Logs` - (ExecDetachedLogsArgs); `Status` - (ExecDetachedStatusArgs); `Kill` - (ExecDetachedKillArgs) | +| `ExecTerminalStatus` | enum | [`ExecTerminalStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L1004) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | +| `ExecDetachedKillOutcome` | enum | [`ExecDetachedKillOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L1195) | `Cancelling`; `AlreadyTerminal` | +| `ShellSessionState` | enum | [`ShellSessionState`](../../packages/d2b-contracts-control/src/public_wire.rs#L1641) | `Attached`; `Detached`; `Killed`; `PoolUnavailable`; `FeatureDisabled`; `OutputGap` | +| `ShellCloseCause` | enum | [`ShellCloseCause`](../../packages/d2b-contracts-control/src/public_wire.rs#L1652) | `ClientDetach`; `EvictedByForce`; `EvictedByAdminDetach`; `KilledByAdmin`; `PoolUnavailable`; `OutputGap` | +| `ConsoleProviderKind` | enum | [`ConsoleProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1762) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | +| `ConsoleOp` | enum | [`ConsoleOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1905) | `Attach` - (ConsoleAttachArgs); `WriteStdin` - (ConsoleWriteStdinArgs); `ReadOutput` - (ConsoleReadOutputArgs); `Resize` - (ConsoleResizeArgs); `Wait` - (ConsoleWaitArgs); `Close` - (ConsoleCloseArgs) | +| `AudioChannel` | enum | [`AudioChannel`](../../packages/d2b-contracts-control/src/public_wire.rs#L2051) | `Speaker`; `Microphone` | +| `AudioEnforcementPosture` | enum | [`AudioEnforcementPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L2065) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | +| `AudioProviderKind` | enum | [`AudioProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2107) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | +| `AudioOp` | enum | [`AudioOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L2152) | `Status` - (AudioStatusArgs); `SetVolume` - (AudioSetVolumeArgs); `Mute` - (AudioMuteArgs) | +| `AudioSetApplied` | enum | [`AudioSetApplied`](../../packages/d2b-contracts-control/src/public_wire.rs#L2215) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | +| `MutatingVerbOutcome` | enum | [`MutatingVerbOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L2341) | `DryRunPlanned`; `Applied`; `ApiReadyTimeout`; `NotYetImplemented`; `BrokerError`; `InvalidRequest` | +| `UsbipProbeStatus` | enum | [`UsbipProbeStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L2485) | `Bound`; `Unbound`; `Degraded`; `Enrollable`; `Enrolled`; `Stale`; `DirectConfig`; `Unknown` | +| `UsbipDurableClaimState` | enum | [`UsbipDurableClaimState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2503) | `Missing`; `HeldByDesiredOwner`; `HeldByOtherOwner`; `StaleOwner`; `Corrupt`; `NotApplicable`; `Unknown` | +| `UsbipHostBindState` | enum | [`UsbipHostBindState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2528) | `Unbound`; `BoundToUsbipHost`; `BoundToUnexpectedDriver`; `DeviceMissing`; `NotApplicable`; `Unknown` | +| `UsbipHostCarrierState` | enum | [`UsbipHostCarrierState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2541) | `Absent`; `Unavailable`; `WithheldForOwner`; `Ready`; `DepartedDuringProbe`; `NotApplicable`; `Unknown` | +| `UsbipProxyState` | enum | [`UsbipProxyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2555) | `NotDeclared`; `Stopped`; `Starting`; `Listening`; `Stale`; `Failed`; `NotApplicable`; `Unknown` | +| `UsbipGuestImportState` | enum | [`UsbipGuestImportState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2578) | `Detached`; `Imported`; `Unavailable`; `NotApplicable`; `Unknown` | +| `UsbipTopologyState` | enum | [`UsbipTopologyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2596) | `Match`; `Mismatch`; `Incomplete`; `NotObserved`; `NotApplicable`; `Unknown` | +| `UsbipPolicyState` | enum | [`UsbipPolicyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2609) | `Allowed`; `Denied`; `Missing`; `NotApplicable`; `Unknown` | +| `UsbipProbeDegradedReasonCode` | enum | [`UsbipProbeDegradedReasonCode`](../../packages/d2b-contracts-control/src/public_wire.rs#L2628) | `PolicyFailed`; `DeviceDepartedBeforeClaim`; `DeviceDepartedAfterLock`; `DeviceDepartedDuringMutation`; `DeviceReappearedWithDifferentTopology`; `LockHeldByOtherOwner`; `InvalidPersistedLockClaim`; `CarrierUnavailable`; `HostBindUnavailable`; `ProxyUnavailable`; `GuestImportUnavailable`; `StaleHostState`; `StaleGuestState`; `ProbeIncomplete`; `Unknown` | +| `UsbProbeEntryKind` | enum | [`UsbProbeEntryKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2664) | `Usbip`; `QemuMediaSlot` | +| `AuditFormat` | enum | [`AuditFormat`](../../packages/d2b-contracts-control/src/public_wire.rs#L2734) | `Human`; `Json` | +| `AuthRole` | enum | [`AuthRole`](../../packages/d2b-contracts-control/src/public_wire.rs#L2742) | `None`; `Launcher`; `Admin` | +| `HostFindingSeverity` | enum | [`HostFindingSeverity`](../../packages/d2b-contracts-control/src/public_wire.rs#L2963) | `Pass`; `Warn`; `Fail` | +| `UsbSkLeaseState` | enum | [`UsbSkLeaseState`](../../packages/d2b-contracts-control/src/public_wire.rs#L4094) | `Idle`; `Active`; `Queued`; `Unknown` | +| `UsbSkSessionOutcome` | enum | [`UsbSkSessionOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L4149) | `Success`; `Timeout`; `Cancelled`; `DeviceUnavailable`; `Active`; `Unknown` | | `TerminalStream` | enum | [`TerminalStream`](../../packages/d2b-contracts-control/src/terminal_wire.rs#L19) | `Stdout`; `Stderr` | | `TerminalStatus` | enum | [`TerminalStatus`](../../packages/d2b-contracts-control/src/terminal_wire.rs#L197) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | | `HelperScopeKind` | enum | [`HelperScopeKind`](../../packages/d2b-contracts-control/src/unsafe_local_wire.rs#L125) | `LauncherApp`; `WaylandProxy`; `PersistentShell` | @@ -657,11 +604,11 @@ running live guest activation. | `Kind` | enum | [`Kind`](../../packages/d2b-contracts/src/error.rs#L17) | `AuthzNotALauncher`; `AuthzAuditRequiresAdmin`; `WireVersionMismatch`; `WireFrameTooLarge`; `WireUnknownField`; `WireIfNameInvalid`; `WireMalformedJson`; `GuestShellDisabled`; `BrokerUnimplemented`; `BrokerValidationFailed`; `ManifestParseError`; `ManifestVersionMismatch`; `InternalIo`; `BundleTampered`; `ProviderMisconfigured`; `ProviderArtifactOutputAmbiguous`; `ProviderArtifactOutputShapeUnknown`; `ProviderRequiredOutputAbsent`; `ProviderRequiredOutputNotRegular`; `ProviderLayoutEntryUnexpected`; `ProviderExecutableNotElf`; `ProviderLaunchFormatRejected`; `ProviderLaunchPermissionDenied`; `ProviderComponentExecutionInvalid`; `ProviderExecutableDeclarationInconsistent`; `ProviderExecutableNotExecutable`; `ProviderSignaturePublisherUnregistered`; `ProviderSignatureIdUnresolvable`; `ProviderSignatureMalformed`; `ProviderSignatureVerificationFailed`; `ProviderDigestMismatch`; `ProviderManifestNotCanonical`; `ProviderExecutableNameInvalid`; `ProviderExecutableSetEmpty`; `ProviderExecutableNotRegular`; `ProviderExecutableSetMismatch`; `ProviderBinaryRefUnresolved` | | `BrokerOp` | enum | [`BrokerOp`](../../packages/d2b-contracts/src/error.rs#L1030) | `ApplyNftables`; `ApplyNmUnmanaged`; `ApplyRoute`; `ApplySysctl`; `BindUnixSocket`; `CreateOrReconcileUsersGroups`; `CreatePersistentTap`; `CreateTapFd`; `DelegateCgroupV2`; `ExportBrokerAudit`; `InjectSecretById`; `LaunchMinijailChild`; `ModprobeIfAllowed`; `OpenCgroupDir`; `OpenDevice`; `OpenFuse`; `OpenKvm`; `OpenVhostNet`; `PauseBroker`; `PrepareRuntimeDir`; `PrepareStateDir`; `MigrateLegacySwtpmState`; `PrepareStoreView`; `ReadSecretById`; `ResumeBroker`; `RotateSecretById`; `SetBridgePortFlags`; `SetSocketAcl`; `SetupMountNamespace`; `UpdateHostsFile`; `UsbipBind`; `UsbipProxyReconcile`; `UsbipUnbind`; `ValidateBundle` | | `IdentityClass` | enum | [`IdentityClass`](../../packages/d2b-contracts/src/identity.rs#L54) | `ZoneId`; `ResourceName`; `ResourceTypeName`; `ResourceUid`; `SessionPurpose`; `ServiceName`; `SchemaFingerprint`; `BindingDigest`; `ResourceBundleGenerationId`; `TranscriptHash`; `Timestamp`; `ResourceGeneration`; `ReconnectGeneration`; `ControllerGeneration`; `ConfigurationGeneration` | -| `KnownFeatureFlag` | enum | [`KnownFeatureFlag`](../../packages/d2b-contracts/src/lib.rs#L104) | `TypedErrors`; `ManifestV04`; `StatusCheckBridges`; `ExportBrokerAudit`; `ConfiguredLaunchV1`; `UnsafeLocalProviderV1`; `CutoverRunnerV1` | -| `W3BrokerOperation` | enum | [`W3BrokerOperation`](../../packages/d2b-contracts/src/privileges_w3.rs#L26) | `DelegateCgroupV2`; `OpenCgroupDir`; `PrepareStateDir`; `PrepareRuntimeDir`; `OpenKvm`; `OpenVhostNet`; `OpenFuse`; `OpenDevice`; `CreateTapFd`; `CreatePersistentTap`; `DeletePersistentTap`; `CreateBridge`; `DeleteBridge`; `SetBridgePortFlags`; `ApplyNftables`; `ApplyNftablesProjection`; `ApplyRoute`; `ApplySysctl`; `ApplyNmUnmanaged`; `UpdateHostsFile`; `BindUnixSocket`; `SetSocketAcl`; `GuestControlSign`; `ModprobeIfAllowed`; `UsbipBindFirewallRule`; `MigrateLegacySwtpmState`; `LaunchCutoverRunner`; `CutoverAudit`; `CutoverEffect`; `SecurityKeyOpenDevice`; `SecurityKeyApplyUdevRules` | +| `KnownFeatureFlag` | enum | [`KnownFeatureFlag`](../../packages/d2b-contracts/src/lib.rs#L103) | `TypedErrors`; `ManifestV04`; `StatusCheckBridges`; `ExportBrokerAudit`; `ConfiguredLaunchV1`; `UnsafeLocalProviderV1` | +| `W3BrokerOperation` | enum | [`W3BrokerOperation`](../../packages/d2b-contracts/src/privileges_w3.rs#L26) | `DelegateCgroupV2`; `OpenCgroupDir`; `PrepareStateDir`; `PrepareRuntimeDir`; `OpenKvm`; `OpenVhostNet`; `OpenFuse`; `OpenDevice`; `CreateTapFd`; `CreatePersistentTap`; `DeletePersistentTap`; `CreateBridge`; `DeleteBridge`; `SetBridgePortFlags`; `ApplyNftables`; `ApplyNftablesProjection`; `ApplyRoute`; `ApplySysctl`; `ApplyNmUnmanaged`; `UpdateHostsFile`; `BindUnixSocket`; `SetSocketAcl`; `ModprobeIfAllowed`; `UsbipBindFirewallRule`; `MigrateLegacySwtpmState`; `SecurityKeyOpenDevice`; `SecurityKeyApplyUdevRules` | | `EntrypointMode` | enum | [`EntrypointMode`](../../packages/d2b-contracts/src/realm.rs#L12) | `HostResident`; `GatewayBacked` | | `RealmControllerPlacement` | enum | [`RealmControllerPlacement`](../../packages/d2b-contracts/src/realm.rs#L26) | `HostLocal`; `GatewayVm`; `CloudFullHost`; `ProviderController` - struct { `provider`: `ProviderId` }; `ProviderAgent` - struct { `provider`: `ProviderId` }; `ProviderSpecific` - struct { `provider`: `ProviderId`; `placement`: `ProtocolToken` } | -| `RuntimeServiceRole` | enum | [`RuntimeServiceRole`](../../packages/d2b-contracts/src/runtime.rs#L99) | `Host`; `Hypervisor`; `Storage`; `Tpm`; `Display`; `Audio`; `Video`; `Network`; `GuestControl`; `Usb`; `Observability` | +| `RuntimeServiceRole` | enum | [`RuntimeServiceRole`](../../packages/d2b-contracts/src/runtime.rs#L98) | `Host`; `Hypervisor`; `Storage`; `Tpm`; `Display`; `Audio`; `Video`; `Network`; `ComponentSession`; `Usb`; `Observability` | | `SecurityKeyVmSessionState` | enum | [`SecurityKeyVmSessionState`](../../packages/d2b-contracts/src/security_key.rs#L165) | `Idle`; `AwaitingLease`; `Active`; `Completed`; `Cancelled` | | `SecurityKeySessionResult` | enum | [`SecurityKeySessionResult`](../../packages/d2b-contracts/src/security_key.rs#L230) | `InProgress`; `Success`; `CtapError`; `Timeout`; `Cancelled`; `InternalError` | | `SecurityKeyEvent` | enum | [`SecurityKeyEvent`](../../packages/d2b-contracts/src/security_key.rs#L290) | `SessionStarted` - struct { `session_id`: `SecurityKeySessionId`; `vm`: `String`; `device_label`: `SecurityKeyDeviceLabel`; `started_at`: `String` }; `SessionSucceeded` - struct { `session_id`: `SecurityKeySessionId`; `vm`: `String`; `device_label`: `SecurityKeyDeviceLabel`; `ended_at`: `String` }; `SessionFailed` - struct { `session_id`: `SecurityKeySessionId`; `vm`: `String`; `device_label`: `SecurityKeyDeviceLabel`; `result`: `SecurityKeySessionResult`; `ended_at`: `String` }; `SessionCancelled` - struct { `session_id`: `SecurityKeySessionId`; `vm`: `String`; `device_label`: `SecurityKeyDeviceLabel`; `ended_at`: `String` }; `DeviceRemoved` - struct { `device_label`: `SecurityKeyDeviceLabel`; `interrupted_session_id`: `Option` }; `DeviceReinserted` - struct { `device_label`: `SecurityKeyDeviceLabel` }; `SessionQueued` - struct { `session_id`: `SecurityKeySessionId`; `vm`: `String`; `device_label`: `SecurityKeyDeviceLabel`; `queued_at`: `String`; `blocking_vm`: `String` } | @@ -740,13 +687,13 @@ the failure class, for example `host check`, `audit`, `status`, or | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1555) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | +| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1486) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | | `HandoffError` | enum | [`HandoffError`](../../packages/d2b-contracts-broker/src/host_generation.rs#L194) | `CompatibilityFloorInvalid`; `GenerationTooOld`; `TargetFingerprintMismatch`; `TargetGenerationMismatch`; `GenerationAncestryInvalid`; `InvalidTransition` | -| `GuestControlError` | struct | [`GuestControlError`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1489) | struct { `kind`: `GuestControlErrorKind`; `remediation`: `HealthRemediation`; `retry_after_ms`: `Option` } | -| `GuestControlErrorKind` | enum | [`GuestControlErrorKind`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1497) | `ProtocolError`; `MaxChunkExceeded`; `StdinBackpressure`; `StdinClosed`; `StdinNotOpen`; `StdinClosedByProcess`; `StdinOffsetMismatch`; `StdinByteBudgetExhausted`; `OffsetExpired`; `OffsetInFuture`; `OffsetExhausted`; `OutputLost`; `TtyStderrUnavailable`; `TtyRequired`; `ExecCapacityExceeded`; `ExecAttachCapacityExceeded`; `ExecNotFound`; `ExecAlreadyExited`; `GuestExecDisabled`; `GuestExecRootDenied`; `GuestExecUserDenied`; `CwdInvalid`; `CwdDenied`; `RetainedLogPathUnsafe`; `RetainedLogQuotaExceeded`; `ReadWaitCapacityExceeded`; `WaitCapacityExceeded`; `SupersededReadWait`; `RateLimited`; `RequestIdConflict`; `ControlSeqMismatch`; `SlowConsumerCancelled`; `StaleSession`; `GuestControlUnavailableOldGeneration`; `AuthFailed`; `TransportUnreachable`; `ExecExpired`; `FileNotFound`; `FileTooLarge`; `PathUnsafe`; `ReadDenied`; `InvalidProgram`; `UsbipUnavailable`; `UsbipCommandFailed`; `UsbipInvalidBusId`; `UsbipInvalidHost`; `GuestShellDisabled`; `ShellInvalidName`; `ShellCapacityExceeded`; `ShellAttachCapacityExceeded`; `ShellNotFound`; `ShellAlreadyAttached`; `ShellPoolUnavailable`; `ShellDaemonEpochMismatch`; `ShellOutputGap`; `UsbipCommandTimeout`; `UsbipInvalidOutput`; `ActivationInvalidId`; `ActivationInvalidPath`; `ActivationInvalidMode`; `ActivationNotFound`; `ActivationStatusUnavailable`; `ActivationTimedOut`; `ActivationSpawnFailed`; `AudioPipeWireUnavailable`; `AudioChannelUnknown`; `AudioLevelOutOfRange`; `AudioEnforcementFailed` | -| `ShellNameError` | struct | [`ShellNameError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1281) | empty struct | -| `AudioErrorKind` | enum | [`AudioErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1859) | `ProviderMisconfigured`; `VmNotFound`; `EnforcementUnavailable`; `AudioNotEnabled`; `InternalError` | -| `AudioVmError` | struct | [`AudioVmError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1960) | struct { `vm`: `String`; `kind`: `AudioErrorKind`; `remediation`: `Option` } | +| `NamedProcessStreamErrorKind` | enum | [`NamedProcessStreamErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1417) | `Authorization`; `StaleSession`; `NotFound`; `Backpressure`; `Protocol`; `Timeout`; `Disconnected` | +| `NamedProcessStreamError` | struct | [`NamedProcessStreamError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1437) | struct { `kind`: `NamedProcessStreamErrorKind` } | +| `ShellNameError` | struct | [`ShellNameError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1510) | empty struct | +| `AudioErrorKind` | enum | [`AudioErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2088) | `ProviderMisconfigured`; `VmNotFound`; `EnforcementUnavailable`; `AudioNotEnabled`; `InternalError` | +| `AudioVmError` | struct | [`AudioVmError`](../../packages/d2b-contracts-control/src/public_wire.rs#L2189) | struct { `vm`: `String`; `kind`: `AudioErrorKind`; `remediation`: `Option` } | | `LevelPercentError` | enum | [`LevelPercentError`](../../packages/d2b-contracts/src/audio.rs#L28) | `OutOfRange` - (u8) | | `AudioPolicyError` | enum | [`AudioPolicyError`](../../packages/d2b-contracts/src/audio.rs#L217) | `InvalidJson` - (String); `InvalidField` - (String); `UnknownSchemaVersion` - (String); `Serialize` - (String) | | `AuditExportErrorCode` | enum | [`AuditExportErrorCode`](../../packages/d2b-contracts/src/audit_wire.rs#L20) | `HashBreak`; `RecordInvalid`; `ReadFailed` | @@ -888,7 +835,7 @@ broker `serve` CLI takes `--audit-dir ` instead of the prior > classification, and `decision` is the local broker operation outcome. `d2bd` also writes daemon-owned JSONL events for transitions that are -not broker operations, such as guest-control exec session establishment +not broker operations, such as ComponentSession exec session establishment and runner readiness failures. Those records live under the daemon state directory as `daemon-events-.jsonl`. Each daemon record carries bounded event metadata plus `prev_hash` and `record_hash` fields so a @@ -898,7 +845,7 @@ an audit sink outage does not abort an already-authorized local operation. Persistent shell daemon events are in this daemon-owned stream, not the broker audit log. The provider-neutral `ShellLifecycle` variant is the sole runtime -shell audit event for both `guest-control` and `unsafe-local`. It records only a +shell audit event for both ComponentSession and `unsafe-local`. It records only a configured canonical target or local VM id, admin peer uid, closed provider/action/result enums, optional force-takeover intent, and optional fixed operation/session correlation digests. It covers create, attach, list, detach, @@ -963,41 +910,3 @@ controlled by `d2b.daemon.autostart.parallelism` (default 3). `autostartParallelism` and `gracefulShutdownTimeoutSeconds`. The latter is the daemon-wide default used when a v7 manifest entry has `lifecycle.gracefulShutdown.timeoutSeconds = null`. -## Cutover admission - -`hostCutover` is an Admin-only public request family. The daemon builds the -authoritative all-Zone inventory for `preview`, validates the exact candidate, -preview, recovery, operator, and consent identities for `apply`, and asks the -live broker to launch one `d2b-cutover-runner` child with a single bootstrap -descriptor, then waits for the runner's authenticated status socket before -reporting admission success. An invalid candidate or source artifact therefore -fails apply rather than producing a success response for a runner that already -exited. It does not own the cutover journal, OFD lock, staged paths, or -repair/adoption logic. `status` and `doctor` are read-only observations after -restart; `hold`, `resume`, `rollback`, `verify`, and `finalize` are sent -directly to the runner socket. Scoped reset admission is separate and binds -its own scope, target, consent, and operation capability. When apply carries a -typed host-generation handoff, `system_artifact_id` is admitted into the -immutable operation request digest, the preview and apply consent bind the -same identity, and the runner refuses any later handoff whose artifact -identity differs. Candidate and source artifact IDs use the canonical v3 -artifact grammar. The broker records the launched runner's process identity -with the capability binding, so an exited runner does not permanently block a -retry while a live runner remains protected from duplicate launch. - -The broker request is `LaunchCutoverRunner`, not `SpawnRunner`. It accepts one -close-on-exec-controlled bootstrap fd and derives the runner executable and -state root from broker configuration. The runner creates a new session and -remains outside the per-VM `d2b.slice` hierarchy. No fourth persistent unit is -declared. After drain, the runner's adapted broker peer is limited to -`CutoverAudit` and the closed `CutoverEffect` vocabulary; it cannot invoke -ordinary daemon operations. Closure activation reuses the typed -`ApplyHostGenerationHandoff` path and refuses stale or foreign bundle state. -The broker binds the bootstrap capability digest at launch and rejects later -runner requests that do not match that operation binding. -Disposition/storage, activation, provider-start, and verification payloads -reuse existing typed broker operations with durable audit evidence. Effects -for quarantine, phase-10 finalization, and scoped durable-Volume destruction -use marker-bound broker adapters with separate consent/authority. Only the -internal `CutoverBroker` phase marker remains non-dispatchable. U6 supplies -only the live rehearsal. diff --git a/docs/reference/daemon-metrics.md b/docs/reference/daemon-metrics.md index bb7c6b6e2c..238bff1e28 100644 --- a/docs/reference/daemon-metrics.md +++ b/docs/reference/daemon-metrics.md @@ -163,18 +163,11 @@ declared schema; see "Cardinality bounds" below. `changes(d2b_daemon_uptime_seconds[5m]) > 0` for a restart alert. -### `d2b_daemon_guest_control_exec_total` +### `d2b_daemon_component_session_shell_total` - **Type:** counter - **Labels:** `subsystem`, `outcome`, `error_kind` -- **Meaning:** Cumulative count of guest-control exec session/op outcomes by - subsystem, closed outcome, and bounded error bucket. - -### `d2b_daemon_guest_control_shell_total` - -- **Type:** counter -- **Labels:** `subsystem`, `outcome`, `error_kind` -- **Meaning:** Cumulative count of guest-control persistent-shell management and +- **Meaning:** Cumulative count of ComponentSession persistent-shell management and attached-owner outcomes. Shell names, session ids, terminal session handles, attach ids, terminal stream ids, provider/resource ids, provider endpoints, provider credentials, process environments, working directories, helper @@ -185,12 +178,11 @@ declared schema; see "Cardinality bounds" below. - **Type:** counter - **Labels:** `provider`, `component`, `operation`, `outcome`, `error_kind` - **Meaning:** Provider-neutral persistent-shell lifecycle outcomes. Providers - are `guest-control` and `unsafe-local`; component is the closed value `shell`; + are `component-session` and `unsafe-local`; component is the closed value `shell`; operations are `list`, `create`, `attach`, `detach`, `kill`, and `close`. Outcomes and error kinds are closed daemon enums. No uid, target, shell name, operation/session id, supervisor metadata, terminal bytes, helper diagnostic, - path, environment, or cwd is a label. The guest-control-specific counter above - remains available for compatibility. + path, environment, or cwd is a label. ### `d2b_daemon_workload_availability` @@ -234,8 +226,8 @@ declared schema; see "Cardinality bounds" below. | `mode` | closed activation mode enum | 4 | | `status` | closed activation phase outcome enum | bounded by daemon code | | `reason` | closed degraded reason enum | bounded by daemon code | -| `subsystem` | closed guest-control subsystem enum | bounded by daemon code | -| `outcome` (guest-control) | closed enum | bounded by daemon code | +| `subsystem` | closed ComponentSession subsystem enum | bounded by daemon code | +| `outcome` (ComponentSession) | closed enum | bounded by daemon code | | `error_kind` | normalized daemon error bucket | bounded by daemon code | | `provider` (shell) | closed shell backend enum | 2 | | `component` (shell) | constant `shell` | 1 | diff --git a/docs/reference/desktop-wrapper.md b/docs/reference/desktop-wrapper.md index ed2b50de7f..54edc90e47 100644 --- a/docs/reference/desktop-wrapper.md +++ b/docs/reference/desktop-wrapper.md @@ -10,7 +10,7 @@ Every graphics-enabled VM (`d2b.vms..graphics.enable = true`) gets an auto-generated `d2b-launch-.desktop` entry installed under `share/applications/`. The wrapper script the entry's `Exec` line points at is the **daemon path** - it drives the VM through -`d2bd → d2b-priv-broker → SpawnRunner`, not the legacy bash +`d2bd → d2b-broker → SpawnRunner`, not the legacy bash `d2b guest start` / `microvm@.service` chain. ## Why a typed contract @@ -66,17 +66,17 @@ of the store. GPU sidecar connects to the host compositor socket that step 1 already waited for; the role-local `/run/d2b-gpu//wayland-0` path is only visible inside the GPU runner's mount namespace. The daemon's - `guest-control-health` DAG node only gates guest-control readiness; + `component-session-health` DAG node only gates ComponentSession readiness; the graphics socket can race slightly behind on cold starts. -4. **Wait for guest-control readiness.** Waits for the VM's - `guest-control-health` gate (the authenticated guest-control Health +4. **Wait for ComponentSession readiness.** Waits for the VM's + `component-session-health` gate (the authenticated ComponentSession Health probe) rather than a raw SSH probe. The historical wrapper polled `@` with the per-VM key; the daemon-native path no longer depends on SSH for terminal access. 5. **Exec Konsole.** Replaces the wrapper with a chromed Konsole running `d2b exec run Guest/ -- `, which creates an - interactive guest-control session (admin-only, runs as the VM's - workload user, no SSH). + interactive ComponentSession backed by a target-local Process (admin-only, + no SSH). `StartupWMClass=org.kde.konsole` matches Konsole's fixed Wayland app_id. @@ -105,7 +105,7 @@ and `journalctl -u d2bd.service`. of scope; that's UX styling, not a lifecycle contract. See `nixos-modules/cli.nix` `desktopItems`. - The in-VM session opened via `d2b exec run` is not pinned - here. The wrapper hands off to Konsole + the guest-control + here. The wrapper hands off to Konsole + the ComponentSession interactive session; what the operator runs once they're in the VM is their concern. - Headless VMs do not get a `.desktop` wrapper at all diff --git a/docs/reference/doctor.md b/docs/reference/doctor.md index 7187975be0..53f10df4e4 100644 --- a/docs/reference/doctor.md +++ b/docs/reference/doctor.md @@ -195,7 +195,7 @@ interval. **Broker replay-buffer depth**: the D7 `ChildReaped` replay-buffer depth (in-memory ring of up to 256 events, used to handle d2bd disconnect/reconnect) is **not yet observable** via a stable CLI command -(`d2b-priv-broker --report-state` is not implemented in v1.2). The +(`d2b-broker --report-state` is not implemented in v1.2). The `data.bufferDepth` field in the JSON output is always `null` for v1.2. When D7 fully lands the IPC mechanism, this field will carry the actual depth and the probe will add Warn (buffer ≥ 200 of 256) and Fail (buffer diff --git a/docs/reference/error-codes.md b/docs/reference/error-codes.md index d15080963e..7f3c1dd5d4 100644 --- a/docs/reference/error-codes.md +++ b/docs/reference/error-codes.md @@ -27,7 +27,6 @@ These anchors document common process exits that are **not** emitted as | `#host-check-warning` | `1` | `host check` reported at least one advisory warning but no required failures. | | `#host-check-failure` | `2` | `host check` reported at least one required failure in its summary output. | | `#not-found` | `70` | Named VM is not declared in the active manifest or the caller is not authorized to know it exists. | -| `#cutover-runner-unavailable` | `69` | The admitted cutover runner socket or transport is unavailable; retry through the operation's owner socket after restoring runner reachability. | ## Typed error catalog @@ -36,10 +35,6 @@ The table below is generated by `bazel run //packages/xtask:xtask -- gen-error-c discriminant with its reserved exit code, owning command, redacted message shape, remediation hint, and docs anchor. -Cutover request validation uses `wire-invalid-frame` for missing, malformed, -or stale known fields. `wire-unknown-field` is reserved for fields that are -not part of the declared request or response schema. - | docs anchor | kind | exit code | owningCommand | message template | remediation | | --- | --- | --- | --- | --- | --- | @@ -57,7 +52,7 @@ not part of the declared request or response schema. | `#manifest-version-mismatch` | `manifest-version-mismatch` | `41` | `status` | manifest {artifact} declared an incompatible manifestVersion (opaque reason: {opaque_reason}) | Re-run `nixos-rebuild switch` against an updated d2b input pinning the daemon's supported manifestVersion. Manifest version changes do not ship a compatibility window. | | `#internal-io` | `internal-io` | `50` | `daemon-api/internal` | an internal I/O step failed (opaque reason: {opaque_reason}) | Retry the command; if the error persists, inspect the daemon logs with the opaque reason token. | | `#bundle-tampered` | `bundle-tampered` | `60` | `bundle-load` | bundle artifact {path} failed tamper-resistance check: {reason} | Re-run `nixos-rebuild switch` to restore the bundle artifacts to their signed state. | -| `#provider-misconfigured` | `provider-misconfigured` | `80` | `provider` | provider for {vm} is misconfigured: {reason} | Check the provider configuration for the VM and verify the expected guestd-compatible agent or sidecar is running. | +| `#provider-misconfigured` | `provider-misconfigured` | `80` | `provider` | provider for {vm} is misconfigured: {reason} | Check the provider configuration for the VM and verify the expected target-local Process or sidecar is running. | | `#provider-artifact-output-ambiguous` | `provider-artifact-output-ambiguous` | `81` | `provider-catalog` | provider artifact {artifact} pins an ambiguous Nix output ({outputs}) | Select the Provider-carrying output explicitly; a non-first raw derivation output is accepted, while a raw derivation's first output must be repackaged. | | `#provider-artifact-output-shape-unknown` | `provider-artifact-output-shape-unknown` | `82` | `provider-catalog` | provider artifact {artifact} has an unrecognized Nix output shape | Supply a derivation or a store path rather than a hand-built package attribute set. | | `#provider-required-output-absent` | `provider-required-output-absent` | `83` | `resource-compiler` | provider artifact {artifact} is missing required output {layout} | Install the required Provider artifact file at its fixed layout-relative path and rebuild. | @@ -240,12 +235,12 @@ avoid the pre-existing CLI exits `2`/`3`/`33`/`78`. | docs anchor | wire kind | exit code | source | meaning | | --- | --- | --- | --- | --- | -| `#exec-transport` | `guest-control-transport-unavailable`, `guest-control-timeout` | `69` | `transport` | The vsock connect / authenticated handshake to the guest was unreachable, or a per-op / establishment deadline elapsed. The `lost-guestd` abnormal terminal maps here too. | -| `#exec-old-generation` | `guest-control-unavailable-old-generation`, `guest-control-capability-unavailable` | `70` | `guest-control` | The VM generation does not advertise guest-control exec, or it lacks a required exec capability. Fail-closed: no SSH fallback. | -| `#exec-capacity` | `exec-session-capacity`, `exec-session-rate-limited` | `75` | `guest-control` | The exec session table is at its global / per-uid / per-vm cap, or the per-uid Start rate limit fired. The `cancelled` / `reaped` abnormal terminals map here too. | -| `#exec-protocol` | `guest-control-protocol-error`, `guest-control-exec-error` | `76` | `protocol` | The guest returned a malformed / out-of-contract response, or deterministically rejected the op. A missing or out-of-range terminal status is also a protocol failure (never synthesized as a guest success). | -| `#exec-auth` | `guest-control-auth-failed`, `authz-not-admin` | `77` | `guest-control` | The authenticated guest-control handshake was rejected, OR the daemon's admin gate refused the caller (`vm exec` is admin-gated). `authz-not-admin` is an authorization failure, not an internal bug, so it maps to the AUTH reserved code rather than the internal default. | -| `#exec-internal` | `guest-control-exec-internal`, any unrecognized exec slug | `42` | `internal` | A daemon-internal or CLI-internal failure driving the session. The `42` default is reserved for genuinely-internal failures only; every known authorization / transport / protocol class above maps to its own reserved code. | +| `#exec-transport` | `component-session-transport-unavailable`, `component-session-timeout`, `component-session-target-lost` | `69` | `transport` | The ComponentSession connection or authenticated handshake to the target was unreachable, or a per-op / establishment deadline elapsed. | +| `#exec-old-generation` | `component-session-unavailable-old-generation`, `component-session-capability-unavailable`, `component-session-exec-detached-unavailable` | `70` | `component-session` | The target generation or signed Process template does not support the requested exec shape. Fail-closed: no alternate transport. | +| `#exec-capacity` | `exec-session-capacity`, `exec-session-rate-limited` | `75` | `component-session` | The exec session table is at its global / per-uid / per-target cap, or the per-uid Start rate limit fired. The `cancelled` / `reaped` abnormal terminals map here too. | +| `#exec-protocol` | `component-session-protocol-error`, `component-session-exec-error`, `component-session-exec-not-found`, `component-session-exec-expired` | `76` | `protocol` | The target returned a malformed / out-of-contract response, deterministically rejected the op, or no longer retains the requested detached record. | +| `#exec-auth` | `component-session-auth-failed`, `authz-not-admin`, `component-session-stale-session` | `77` | `component-session` | The authenticated ComponentSession evidence was rejected, the daemon's admin gate refused the caller, or a stale session was detected. | +| `#exec-internal` | `component-session-exec-internal`, any unrecognized exec slug | `42` | `internal` | A daemon-internal or CLI-internal failure driving the session. The `42` default is reserved for genuinely-internal failures only; every known authorization / transport / protocol class above maps to its own reserved code. | `vm exec --json` always emits exactly one terminal JSON document on stdout for every outcome (including early transport / auth / @@ -371,7 +366,7 @@ remediation block renders as: ``` Remediation: d2bd is not reachable. Start the daemon and re-run: - sudo systemctl start d2b-priv-broker.socket + sudo systemctl start d2b-broker.socket sudo systemctl start d2bd.service For the full operator runbook context, see: docs/how-to/migrate-d2b-v0-to-v1.md @@ -400,12 +395,3 @@ Their golden files (`console-deferred.golden`, `audio-deferred.golden`) are retired. The Rust `Display` impl for the two remaining deferred verbs is verified byte-for-byte against `tests/golden/cli-output/audit-*-deferred.golden` and `keys-deferred.golden`. -## Cutover refusals - -The cutover surface fails closed before mutation for incomplete all-Zone -inventory, stale preview or candidate digests, missing recovery evidence, -wrong Admin operator binding, unavailable privileged audit durability, active -holds, unavailable runner sockets, and foreign journal or lock ownership. -One-time `--zone` selection is a usage error. A broker that has not yet -received the trusted runner registration returns `broker-unimplemented`; the -CLI never falls back to a shell command. diff --git a/docs/reference/guest-control-exec-interactive-tty.md b/docs/reference/guest-control-exec-interactive-tty.md deleted file mode 100644 index a7089053ab..0000000000 --- a/docs/reference/guest-control-exec-interactive-tty.md +++ /dev/null @@ -1,177 +0,0 @@ -# Guest control exec I/O: interactive TTY exec - -This document specifies the interactive (PTY-backed) exec path of the -guest-control plane. It is the design follow-up to the interactive-exec -section of [ADR 0028](../adr/0028-guest-control-plane-over-vsock.md) and -builds on the non-interactive contract in the -[chunked stdio reference](./guest-control-exec-io-chunked-stdio.md). - -> **Scope note.** This document specifies the interactive TTY exec -> *RPC/service* surface served by `d2b-guestd`. The operator-facing -> `d2b exec attach` / `--tty` **CLI** front-end is shipped and -> drives this contract (admin-only, over the authenticated -> guest-control vsock; see -> [`cli-contract.md`](./cli-contract.md) for the verb surface). - -## Mode selection - -`exec_create` selects the interactive path from two request fields: - -| `tty` | `detach` | Path | -| ----- | -------- | ---- | -| `false` | `false` | Non-interactive **attached** exec (separate stdout/stderr, 6 h ceiling). | -| `false` | `true` | Non-interactive **detached** exec (slot-keyed, retained logs). | -| `true` | `false` | Interactive **TTY** exec (this document). | -| `true` | `true` | Rejected: typed `ProtocolError` (unsupported mode). | - -An interactive TTY exec is **connection-owned and non-durable**: it lives only -for the originating ttRPC connection, has no retained-log or registry record, -and is torn down when the connection drops. It is served entirely by -`d2b-guestd`; there is no per-user `d2b-userd` involvement. - -## Spawn model: helper-exec, no first-party unsafe - -guestd never performs the controlling-terminal handshake and never acquires a -controlling terminal. The full guest stack keeps `unsafe_code = "forbid"`. - -1. guestd allocates the PTY master with `posix_openpt(O_RDWR|O_NOCTTY| - O_CLOEXEC)`, then `grantpt` / `unlockpt` / `ptsname`, and opens the slave - `O_RDWR|O_NOCTTY|O_CLOEXEC`. The slave's `O_CLOEXEC` is intentional: a - concurrent fork/exec elsewhere in guestd cannot inherit the slave and keep - the PTY alive (preserving the HUP/EOF contract). `Stdio::from(slave)` still - hands fd 0 to the helper because `Command`'s `dup2` clears `CLOEXEC` on the - duplicate. -2. guestd spawns the static `d2b-exec-runner` in its `--tty-exec` mode via - `Command`, with the slave on **stdin** (`Stdio::from(OwnedFd)`) and an - `O_CLOEXEC` status pipe on **stdout**. There are no arbitrary `pass_fds`, - no `process_group(0)`, and no `pre_exec` closure. -3. The helper (safe `rustix`) `F_DUPFD_CLOEXEC`s the status fd to a high - number (so the later `dup2` onto fd 1 cannot clobber it), `setsid()`s to - become a session leader with no controlling terminal, acquires the slave as - its controlling terminal (`TIOCSCTTY`), applies the initial geometry - (`tcsetwinsize`), `dup2`s the slave onto fds 0/1/2, and `execve`s the - target argv. -4. On success the `O_CLOEXEC` status fd closes during `execve`; guestd - observes EOF on the status pipe (success handshake). On any setup or `exec` - failure the helper writes one typed status byte and exits; guestd maps that - byte to a typed `ExecCreate` error. - -Because the helper is a session leader and `TIOCSCTTY`'d the slave, the -spawned process is the session leader (`sid == pid`) and the foreground -process group of the terminal. - -## Output: merged, stderr disabled - -A PTY exposes a single output side, so stdout and stderr are **merged** onto -the stdout stream and read through the normal `ReadOutput(stream=stdout)` -cursor model. The stderr ring is pre-marked EOF at create, so -`ReadOutput(stream=stderr)` on a TTY exec returns a typed -**stderr-unavailable** error rather than blocking or returning data. - -## Initial terminal geometry - -`initial_terminal_size` is optional: - -- **absent** ⇒ the helper applies a default of **24 rows × 80 cols**; -- **present** ⇒ both dimensions must be in `1..=65535`; `0` or out-of-range - is rejected (validation / protocol error). - -## Stdin and CloseStdin (VEOF) - -`WriteStdin` uses the same machine as non-interactive exec: a monotonic byte -offset (duplicate / out-of-order offsets are rejected), serialized writes, and -bounded backpressure (non-blocking writes, `WouldBlock`/partial-write handling, -a bounded queue, and per-connection handler + decoded-byte budgets). No PTY fd -lock is held across an `await`. - -`CloseStdin` **injects VEOF (`0x04`)** into the PTY line discipline and keeps -the master open. It is **not** a master-close half-close (the master is closed -only on disconnect, cancel, or terminal teardown). `CloseStdin` is idempotent -(a duplicate `CloseStdin` at the same final offset is a no-op success that does -**not** inject a second VEOF), and any subsequent `WriteStdin` is rejected with -a typed **stdin-closed** error. `WriteStdin` with `close_after=true` writes the -data, then injects VEOF. - -A `0x04` byte sent through `WriteStdin` is **ordinary stdin data**, not a -protocol close: guestd forwards it to the PTY like any other input. In raw mode -the foreground program receives the literal `0x04`; under canonical mode the -line discipline interprets it (as the `VEOF` control char) exactly as a real -terminal would. Only the explicit `CloseStdin` RPC (or `close_after=true`) -performs the protocol-level stdin close. - -## Resize and signal ordering - -`TtyWinResize` and `ExecSignal` are serialized through the same per-exec, -**strictly-increasing** `control_seq` dispatcher. A sequence number ≤ the last -accepted value (stale, duplicate, or out-of-order) is rejected; gaps are -allowed. - -`ExecSignal` is **TTY-only** and enforces two allowlists: - -- **Target allowlist.** Only `FOREGROUND_PROCESS_GROUP` is accepted; any other - `SignalTarget` is rejected with a `ProtocolError` **before** the sequence is - consumed. The foreground process group is resolved via `tcgetpgrp(master)` - **at delivery time** (so it tracks job-control changes inside the session). - Non-TTY `PROCESS_TREE` signalling is **deferred** - it is not implemented in - this surface, so a `PROCESS_TREE` (or unspecified/unknown) target is rejected - with the same typed error. -- **Signal allowlist.** The delivered signal must be one of - `INT`, `TERM`, `HUP`, `QUIT`, `WINCH`, `USR1`, `USR2`, `KILL`, `TSTP`, - `CONT`. An out-of-allowlist signal number is rejected **before** the - sequence is consumed - the `control_seq` is not advanced, so it stays - available for a subsequent valid control message - and maps to a typed - `ProtocolError`. There is no dedicated invalid-signal wire kind. - -## Runtime ceiling: indefinite, scoped to TTY - -A TTY exec runs **indefinitely by default** (`interactiveMaxRuntimeSec = 0` ⇒ -unlimited). Setting `d2b.vms..guest.exec.interactiveMaxRuntimeSec > 0` -installs an optional ceiling for interactive sessions only. The 6-hour -non-interactive attached ceiling (`MAX_EXEC_RUNTIME_MS`) is unchanged - only -the interactive path opts into unlimited runtime. - -## Teardown: Running → Closing → Terminal - -Teardown is a three-state machine driven by child exit, ceiling expiry, -explicit cancel, or host disconnect, and is idempotent (the first caller to -enter `Closing` wins the race): - -1. **Running → Closing.** Atomically reject new stdin/control RPCs (typed - no-op/error, no side effect), drop pending accepted writes, stop issuing - new master clones, and release handles. -2. Drop the last master reference, which delivers `SIGHUP` to the session - leader. -3. Wait a bounded grace period. -4. `SIGKILL` the whole TTY **session**: enumerate the session's processes by - `sid` via `/proc` and signal them, repeating until the session is empty. -5. Reap and set the terminal `ExecState` (cancelled vs exited/signaled), then - notify waiters. - -### In-session no-orphan limitation - -The no-orphan guarantee is **scoped to processes that remain in the exec's -session**. A child that deliberately escapes the session via `setsid()` or a -double-fork is **not** reaped by the session sweep. This is a documented -trusted-root limitation: the interactive exec target already runs with the -guest user's privileges, so escaping the session is not a privilege boundary. - -## Capabilities - -`ExecTty`, `TtyResize`, and `Signals` are advertised in the capabilities -response **only when the interactive path is usable** - i.e. the PTY spawner -is wired, which requires the `d2b-exec-runner` helper to be present -(the same gate as the detached exec surface). - -## Conformance - -The interactive contract is validated by: - -- a fake-driven runtime matrix in `d2b-guestd` (fake PTY duplex, fake - session reaper, and fake clock) covering offset/sequence rejection, VEOF - injection, target/signal allowlists, the runtime ceiling, and the - `Running → Closing → Terminal` teardown; and -- a real-PTY, Linux-only integration test in `d2b-exec-runner` that drives - the `--tty-exec` helper exactly as guestd does and asserts session - leadership + controlling terminal, the initial winsize, `SIGWINCH` - delivery on resize, `SIGHUP` on master hangup, status-pipe EOF (no fd - leak), and in-session reap. diff --git a/docs/reference/guest-control-exec-io-chunked-stdio.md b/docs/reference/guest-control-exec-io-chunked-stdio.md deleted file mode 100644 index f0b8ae1d59..0000000000 --- a/docs/reference/guest-control-exec-io-chunked-stdio.md +++ /dev/null @@ -1,1218 +0,0 @@ -# Guest control exec I/O: chunked stdio ttRPC design - -This document specifies the bounded exec I/O protocol selected for the -guest-control design: ttRPC unary calls for lifecycle and -Kata-style chunked stdio RPCs for stdin/stdout/stderr. It is the design -follow-up to [ADR 0028](../adr/0028-guest-control-plane-over-vsock.md) -and the [guest-control feasibility dossier](../adr/guest-control-feasibility-dossier.md). - -## Decision summary - -Use one authenticated ttRPC connection per host-to-guest control session. -Exec lifecycle, inspection, wait, signal, resize, and stdio movement are -all unary ttRPC calls. Stdio is not represented as raw ttRPC streams. -Instead, each caller transfers bounded chunks with explicit cursors: - -- `WriteStdin` appends one bounded stdin chunk if the guest has capacity. -- `ReadOutput` returns bounded stdout or stderr chunks at requested byte - offsets from server-owned append-only per-exec logs. -- `CloseStdin` half-closes stdin exactly once. -- `TtyWinResize` and `Signal` are ordered control events on the exec's - control sequence. -- `Wait` long-polls for terminal process state, and `Inspect` returns the - current state without consuming output. - -This follows Kata Agent's prior-art shape (`WriteStdin`, `ReadStdout`, -`ReadStderr`, `CloseStdin`, `TtyWinResize`, `SignalProcess`, and -`WaitProcess`) while adding d2b-specific offset cursors, memory -budgets, long-poll behavior, detached-log retention, and typed -slow-consumer cancellation. - -## Service surface - -The committed protobuf service source is -[`packages/d2b-contracts-control/guest_control.proto`](../../packages/d2b-contracts-control/guest_control.proto). -Lifecycle requests carry `vm_id`, `request_id`, and negotiated -`protocol_version` in `RequestMetadata`; exec-specific requests also carry -`exec_id` and the Hello-returned `guest_boot_id` in `ExecRequestMetadata` so -VM reboot or guestd restart changes reject stale sessions instead of -reattaching to an unrelated reused exec id. - -```protobuf -service GuestControl { - rpc Hello(HelloRequest) returns (HelloResponse); - rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse); - rpc Capabilities(CapabilitiesRequest) returns (CapabilitiesResponse); - rpc Health(HealthRequest) returns (HealthResponse); - - rpc ExecCreate(ExecCreateRequest) returns (ExecCreateResponse); - rpc ExecInspect(ExecInspectRequest) returns (ExecInspectResponse); - rpc ExecWait(ExecWaitRequest) returns (ExecWaitResponse); - rpc ExecLogs(ExecLogsRequest) returns (ExecLogsResponse); - rpc ExecList(ExecListRequest) returns (ExecListResponse); - - rpc WriteStdin(WriteStdinRequest) returns (WriteStdinResponse); - rpc ReadOutput(ReadOutputRequest) returns (ReadOutputResponse); - rpc CloseStdin(CloseStdinRequest) returns (CloseStdinResponse); - rpc TtyWinResize(TtyWinResizeRequest) returns (ControlAck); - rpc ExecSignal(ExecSignalRequest) returns (ControlAck); - rpc ExecCancel(ExecCancelRequest) returns (ControlAck); -} -``` - -`Hello` is unauthenticated challenge/discovery only: it returns the guest -nonce, guest boot id, and protocol version, but no health, capabilities, or -capability fingerprint. `Authenticate` is the proof-of-possession boundary: -the host sends a fixed-size HMAC-SHA256 tag over the canonical transcript and -the guest returns its HMAC plus bounded authenticated Health and Capabilities. -All later `Health`, `Capabilities`, and exec RPC handlers require the -authenticated per-connection session. - -`ExecLogs` is a convenience wrapper for old/detached execs. Attached -clients use `ReadOutput(stream=stdout|stderr)` directly so polling state, -cursors, and backpressure are identical for attached and detached sessions. - -`ExecList` is a minimal, read-only discovery RPC for detached execs. It -enumerates the caller's detached records for the same VM token + boot -(bounded ≤32) so a host caller whose `ExecCreate` reply was lost -(crash-after-dispatch, re-adopted) can still find and operate on the exec by -id. Each `ExecListEntry` carries `exec_id`, `slot`, `state`, -`create_time_unix`, an **`argv_sha256`** hex digest (never raw argv, -environment, or cwd), and the per-stream `stdout_truncated`/`stderr_truncated` -booleans plus `dropped_bytes`. `ExecList` reconciles on access (a listed -vanished job shows `cancelled`/lost, not stale `running`), authenticates -before any side effect, and never lists attached execs. The CLI and public -daemon DTOs do not expose the argv hash. - -A detached exec lookup distinguishes three not-available conditions with -distinct typed error kinds: - -- `stale-session` - the request's `guest_boot_id` does not match the current - boot (re-adoption is bounded to a single boot); -- `exec-expired` - the record existed for this caller but was evicted by the - retention TTL / GC (tombstoned); -- `exec-not-found` - no such exec id exists for this caller. - -## Exec lifecycle messages - -### `ExecCreate` - -Request fields: - -- `argv`: repeated string, already split by the CLI after `--`. -- `user`: ignored/reserved legacy field. Shipped `guestd` always runs - exec as the host-fixed workload user from `ssh.user`; clients cannot - select a different target identity through this wire field. -- `cwd`: optional absolute path. -- `env`: repeated key/value entries after host-side policy filtering. -- `tty`: bool. When true, stdout and stderr are PTY-merged into stdout; - `ReadOutput(stream=stderr)` returns `tty-stderr-unavailable`. -- `stdin_open`: bool. Defaults false; the CLI sets it true only for a - `-t`/`--tty` (interactive PTY) session. -- `detached`: bool. Detached execs persist bounded logs after caller - disconnect. -- `initial_terminal_size`: optional `{rows, cols}`, valid **only** for - `tty=true` execs. When absent it defaults to `24` rows by `80` cols; when - present each dimension must be in the Linux `winsize` range `1..=65535`. A - present `0` or out-of-range value is rejected with `protocol-error` (it is - never silently defaulted). Presence on a non-`tty` exec is **rejected** with - `protocol-error` (unsupported mode) - it is never silently ignored. -- `output_policy`: `{max_chunk_bytes, max_stdout_log_bytes, - max_stderr_log_bytes, slow_consumer_timeout_ms, wait_timeout_ms}`. The - server clamps each value to the VM capability maximum. - -Response fields: - -- `exec_id`. -- `created_at_monotonic_ns` and `control_seq` initially `0`. -- `stdout_cursor` and `stderr_cursor` initially `0`. -- `effective_limits`: the clamped protocol limits. -- `state`: `created` or typed failure. - -`ExecCreate` starts the guest process only after the session object, -stdio pipes or PTY, log buffers, and event queue have been allocated. -For TTY execs, the PTY is opened with the initial geometry before spawn, -so the child observes the correct size from its first instruction. - -### `ExecInspect` - -Returns non-consuming state: - -- state enum: `created`, `running`, `exited`, `signaled`, `cancelled`, - `slow-consumer-cancelled`, `protocol-error`, `lost-guestd`, `reaped`; -- `visible_terminal_status` when terminal. It is a oneof-style outcome: - `{outcome = "exit-code", exitCode}`, `{outcome = "signal", signal}`, - `{outcome = "status-code", statusCode}`, or - `{outcome = "error", error}`; -- `stdin_state`: `open`, `closing`, `closed`, `closed-by-process`, - `rejected-not-interactive`; -- `stdout_start_offset`, `stdout_end_offset`, `stderr_start_offset`, - `stderr_end_offset`; -- `stdout_dropped_bytes`, `stderr_dropped_bytes`, - `stdout_truncated_for_retention`, and - `stderr_truncated_for_retention`; -- `last_control_seq`, `state_generation`, and any bounded `error`. - -The start/end offsets define the valid read window. If a caller asks for -an offset below `*_start_offset`, the server returns `offset-expired` -with the new start offset rather than silently serving newer bytes. - -Terminal status has two forms: - -- `recorded_terminal_status`: the child exit/signal/cancel result guestd - has observed internally. This is not returned to host callers until it - becomes visible. -- `visible_terminal_status`: populated only after every output byte guestd - read before terminal observation is either retained in the stream/log - cursor window, already delivered/acknowledged by the attached reader, or - explicitly represented by detached `start_offset`/`dropped_bytes` - accounting. Until then, `ExecInspect` and `ExecWait` report a - non-terminal state with `visible_terminal_status = null` and the stream - end offsets needed to discover the output first. - -Detached execs may record terminal status before a client reads retained -logs, but `visible_terminal_status` still requires retained-log cursor or -explicit dropped-byte metadata that makes any preceding output discoverable -through `ExecLogs`. Attached CLI flows use `visible_terminal_status` for -process exit and local raw-mode cleanup once their read cursor has consumed -or can still read the preceding output. - -If guestd loses pre-terminal output without delivering it to the attached -reader or representing it through detached `start_offset`/`dropped_bytes` -accounting, the exec transitions to terminal `protocol-error` with bounded -kind `output-lost`. This wakes waiters/readers and lets the CLI restore -local terminal state with a typed d2b error instead of polling -forever. - -### `ExecWait` - -Request fields: - -- `timeout_ms`: clamped by `effective_limits.wait_timeout_ms`. -- `known_state_generation`: optional value from a previous inspect/wait. - -Response fields: - -- current terminal/non-terminal state; -- `visible_terminal_status`, populated only after preceding output is - available through the returned stdout/stderr offset window; -- `state_generation`; -- current stdout/stderr offset window. - -If the process is still running, or terminal status is recorded but not -yet visible because preceding output has not drained into the retained -cursor window, `ExecWait` holds the unary request until the timeout -expires or state/visibility changes. Timeout is not an error; it returns -the current state with `timed_out: true`. Client-side RPC cancellation -cancels only that wait call, not the exec. - -## Stdio message shapes - -### `WriteStdin` - -Request fields: - -- `request_id`: idempotency key for this stdin write. -- `offset`: caller's stdin byte offset for idempotency. -- `data`: bytes, length `1..max_chunk_bytes`. -- `close_after`: required bool for atomic final chunk plus half-close; callers - send `false` when they only want to write bytes. -- `client_deadline_ms`: optional deadline for waiting on bounded guestd - stdin-queue capacity. - -Response fields: - -- `accepted_offset` and `accepted_len`; -- `next_offset`; -- `stdin_state`; -- `blocked_ms`; -- `disposition`: `accepted`, `duplicate`, or `rejected`; -- `error`, populated for typed retryable failures such as - `stdin-offset-mismatch`, `stdin-backpressure`, `request-id-conflict`, and - `stdin-byte-budget-exhausted`. - -Rules: - -1. The ttRPC receiver enforces its fixed 4 MiB frame limit before - protobuf decode, and `data.len()` must not exceed - `effective_limits.max_chunk_bytes` after decode. Because - `ttrpc-rust` 0.9.x allocates frames up to that fixed transport limit - before handler admission, the decoded request is admitted only while - holding the per-connection decoded-byte budget and the per-exec stdin - write permit described in - [Receive limits and concurrency proof](#receive-limits-and-concurrency-proof). -2. `offset` must equal the server's next expected stdin offset, unless it - exactly repeats the previous accepted chunk and `request_id` matches a - dedupe entry. Otherwise the server returns `stdin-offset-mismatch` - with the expected offset. -3. `WriteStdin` is all-or-nothing at the RPC boundary. Guestd either - copies the entire chunk into its bounded stdin queue and returns - `accepted_len = data.len()`, or returns `stdin-backpressure` with - `accepted_len = 0` and leaves `next_offset` unchanged. -4. Guestd's child-stdin writer drains that queue into the Linux pipe or - PTY and tracks partial kernel writes internally. A partial pipe/PTY - write is never exposed as a retryable offset-stable failure, so a - retry cannot duplicate bytes already delivered to the child. -5. If the bounded queue cannot accept the whole chunk, the server waits - only until `min(client_deadline_ms, stdin_write_deadline_ms)`. On - expiry it returns `stdin-backpressure` before accepting any bytes. -6. If stdin is already closed, the call returns `stdin-closed` and the - expected offset. -7. In non-interactive execs, all writes return `stdin-not-open`. -8. `close_after` commits atomically with the accepted bytes. The writer - drains the accepted bytes first, then applies the same endpoint-specific - close semantics as `CloseStdin`: pipe-backed execs close child stdin - after the queue drains, while TTY execs mark protocol input closed - without closing the PTY master/writer or stopping output reads. If the - chunk cannot be accepted, stdin remains open and the caller retries or - calls `CloseStdin`. - -**Interactive TTY carve-out.** The TTY path does **not** use the -all-or-nothing + `request_id`-replay model of rules 2-4. Instead: - -- **Partial accepted_len.** A TTY `WriteStdin` reports `accepted_len` equal to - the bytes durably written to the PTY master, which may be **less than** - `data.len()` (a partial kernel write), and advances `next_offset` by exactly - that count. The caller retries the remainder from `next_offset`; no byte is - re-delivered. A write that makes **zero** progress on a non-empty payload - (the master is gone) returns `stdin-closed`, leaving `next_offset` unchanged. -- **Strict contiguous offset, no dedupe.** A TTY write is admitted only when - `offset` equals the next expected offset. There is **no** `request_id` dedupe - entry on the TTY path, so a duplicate or out-of-order offset (including a - replay of an already-accepted chunk) is rejected with `stdin-offset-mismatch` - and the expected offset - it is never silently absorbed as a duplicate. -- **VEOF is not counted.** `close_after=true` (and `CloseStdin`) inject `VEOF` - only once the full payload has landed; the injected `VEOF` control byte does - not advance `next_offset`. - -### `CloseStdin` - -Request fields: - -- `request_id`: idempotency key for the close operation. -- `offset`: the caller's final stdin offset. - -Response fields: - -- `stdin_state`, `final_offset`, and `disposition`; -- `error`, populated for typed failures such as `stdin-closed`, - `request-id-conflict`, or `stdin-offset-mismatch`. - -Rules: - -- The close is accepted only when `offset` equals the next expected stdin - offset. -- Close is idempotent only for the same retained `request_id` and final - offset. Reusing the same `request_id` with a different offset returns - `request-id-conflict`; a different `request_id` after stdin is already - closed returns `stdin-closed`. -- For pipe-backed non-TTY execs, the server schedules EOF and closes the - child's stdin fd only after all already accepted stdin bytes through the - final offset have drained from the bounded queue. -- For TTY execs, `CloseStdin` injects the line-discipline `VEOF` byte - (`Ctrl-D` / `0x04`) into the PTY master once all accepted stdin bytes - through the final offset have landed, then marks stdin closed. A PTY master - has no true write half-close (closing it would `SIGHUP` output and control), - so guestd models stdin half-close as this single `VEOF` inject and keeps the - master open. Under canonical mode the foreground reader observes EOF; in raw - mode `Ctrl-D` is ordinary input. `WriteStdin(close_after=true)` writes the - payload and then performs the same `VEOF` inject. -- TTY close keeps the PTY master open: guestd does not drop the writer handle, - send `SIGHUP`, or stop the output reader on `CloseStdin`. The master is closed - only on caller disconnect, cancel, or a terminal state. Subsequent - `WriteStdin` calls are rejected with `stdin-closed`; a duplicate `CloseStdin` - at the same final offset is an idempotent no-op success that does not inject a - second `VEOF`. Output already buffered or produced later by the foreground - program remains readable until PTY output EOF. -- A child closing its read side transitions to `closed-by-process`; later - writes get `stdin-closed-by-process`. - -### `ReadOutput` - -Request fields: - -- `offset`: absolute byte offset in the stream log. -- `stream`: `stdout` or `stderr`; TTY execs reject `stderr` with - `tty-stderr-unavailable`. -- `max_len`: requested maximum bytes, `1..max_chunk_bytes`; larger or - zero-length requests fail with a typed protocol error. -- `wait`: bool. -- `timeout_ms`: long-poll duration if `wait` is true. - -Response fields: - -- `stream`: `stdout` or `stderr`; -- `offset`: the request offset served; -- `data`: bytes, length `0..max_len`; -- `next_offset`; -- `start_offset` and `end_offset` after the read; -- `eof`: true only when process output for that stream is complete and - `next_offset == end_offset`; -- `truncated`: true if any older bytes were dropped by retention; -- `timed_out`: true for long-poll timeout with no bytes; -- `error`, populated for typed failures such as `offset-expired`, - `offset-in-future`, `output-lost`, or `tty-stderr-unavailable`. - -Rules: - -1. `max_len` is validated before allocation and must be non-zero and at - most `max_chunk_bytes` after clamping. -2. If `offset < start_offset`, the server returns `offset-expired` with - `start_offset`; the client decides whether to fail the command or - resume from the retained prefix boundary for log viewing. -3. If `offset > end_offset`, the server returns `offset-in-future`. -4. If data is available at `offset`, return immediately with up to - `max_len` bytes. -5. If no data is available and EOF is not reached, `wait=true` holds the - unary call until bytes arrive, EOF arrives, process state changes, or - timeout. Timeout returns an empty successful response with - `timed_out: true`. -6. EOF is sticky and idempotent. Reads at `end_offset` after EOF return - empty data with `eof: true`. -7. For PTY output, Linux may surface slave closure as `EIO` on master - reads and readiness APIs may report hangup. Guestd treats those as the - PTY-output EOF edge only after the output reader has appended every - byte returned before the `EIO`/hangup. `ReadOutput(stdout)` must therefore - report `eof: true` only when `next_offset == end_offset` after that - drain; it must not translate the first hangup into premature EOF or - discard buffered tail bytes. - -## Cursors, offsets, and idempotency - -All stdin/stdout/stderr offsets are unsigned 64-bit absolute byte offsets -per exec and per stream. They never wrap during a retained exec; reaching -`u64::MAX` is a fatal `offset-exhausted` protocol error that cancels the -exec. The server maintains: - -- `stdin_next_offset`: next expected `WriteStdin`/`CloseStdin` offset; -- `stdout_start_offset` / `stdout_end_offset`; -- `stderr_start_offset` / `stderr_end_offset`; -- a bounded `request_id` dedupe ring for recently accepted stdin writes, - close, signal, resize, and cancel operations. - -Read calls are naturally idempotent because offsets address immutable -log bytes. Write/control calls are idempotent only for the same -`request_id` and same payload hash while their dedupe entry is retained. -A duplicate with mismatched payload is `request-id-conflict`. - -## Chunk and buffer limits - -Default design limits: - -| Limit | Default | Hard maximum | Notes | -| --- | ---: | ---: | --- | -| `max_chunk_bytes` | 64 KiB | 1 MiB | Application chunk limit; larger decoded chunks are rejected before session-buffer copy. | -| ttRPC frame cap | 4 MiB | 4 MiB | Fixed by `ttrpc-rust` 0.9.x before handler/protobuf admission; lowering it requires a wrapper or patch and a new proof. | -| chunked-stdio connections per VM | 4 | 4 | Bounds cross-connection decoded stdin pressure. | -| stdin in-flight/queue per exec | 1 chunk | 1 MiB | One decoded request may feed one bounded guestd stdin queue; pipe/PTY partial writes are tracked behind that queue. | -| decoded WriteStdin bytes per connection | 16 MiB | 16 MiB | Four concurrent ttRPC frames at the fixed 4 MiB cap; effective chunks above 1 MiB are rejected before session-buffer copy. | -| concurrent WriteStdin handlers per connection | 4 | 4 | Bounds protobuf `bytes` allocations even for malicious fan-in. | -| stdout live buffer per stream | 1 MiB | 8 MiB | Attached sessions; producer blocks before exceeding it. | -| stderr live buffer per stream | 1 MiB | 8 MiB | Not used for TTY execs. | -| detached stdout log | 4 MiB | 4 MiB | Per-stream slot-keyed file, drop-oldest by offset with truncation accounting. | -| detached stderr log | 4 MiB | 4 MiB | Separate in non-TTY mode; same per-stream cap. | -| long-poll read timeout | 100 ms | 1 s | CLI can immediately re-poll for interactive use. | -| slow-consumer grace | 30 s | 5 min | Must satisfy the slow-consumer proof. | -| concurrent exec sessions per VM | 32 | 256 | New sessions fail before spawn with `exec-capacity-exceeded`. | -| attached exec sessions per VM | 8 | 64 | New attaches fail without affecting detached execs. | -| pending `ReadOutput(stdout)` waits | 1 per exec/connection, 64 per VM | 512 per VM | Duplicate waits are superseded or rejected. | -| pending `ReadOutput(stderr)` waits | 1 per exec/connection, 64 per VM | 512 per VM | TTY mode always returns `tty-stderr-unavailable`. | -| pending `ExecWait` calls | 1 per exec/connection, 64 per VM | 512 per VM | Duplicate waits are superseded or rejected. | -| RPC rate budget | 200/s per connection, 1000/s per VM burst | policy-defined | Excess calls return `rate-limited` with bounded retry-after. | - -The hard maximums are capability values returned by `Authenticate` and -`Capabilities` after the guest-control token proof succeeds. They -may be lowered by VM policy. Raising them requires updating tests and -memory-budget documentation. - -Proto3 presence is explicit where absence changes semantics: -`ExecCreateRequest.user`, `ExecCreateRequest.cwd`, -`ExecCreateResponse.exec_id`, `ExecWaitRequest.known_state_generation`, -`WriteStdinRequest.client_deadline_ms`, and -`GuestControlError.retry_after_ms` are `optional` in the protobuf source and -nullable in the generated JSON schema. Plain scalar zero/empty values are not -used as implicit absence sentinels for those fields. - -## Receive limits and concurrency proof - -The selected invariant is **bounded post-decode allocation**, with a -transport prefilter where ttRPC exposes one. A protobuf `bytes` field can -be allocated by the generated decoder before the `WriteStdin` handler -examines `data.len()`, so the design does not claim that every -`max_chunk_bytes + 1` request is rejected before all allocation. Instead, -the receiver enforces these gates, in order: - -1. Use `ttrpc-rust` 0.9.x's fixed `MESSAGE_LENGTH_MAX = 4 MiB` as the - selected transport pre-decode bound. Messages larger than that are - rejected by ttRPC framing; messages at or below that bound may allocate - a ttRPC payload buffer before handler/protobuf admission. Lowering this - bound requires a pre-ttRPC frame limiter, patched/configurable ttRPC - transport, and a new proof. -2. Limit chunked-stdio decode/handler concurrency per connection to four - `WriteStdin` calls. With the transport cap above, a malicious client - can force at most `4 * 4 MiB = 16 MiB` decoded request bytes per - connection before application admission runs. -3. At handler entry, charge `data.len()` against a per-connection - weighted semaphore with a 16 MiB budget. The permit is acquired before - hashing for idempotency, copying into any session buffer, or waiting on - bounded stdin-queue capacity, and is released when the response is sent. - Exhaustion returns typed `stdin-byte-budget-exhausted` without - retaining the payload. -4. Acquire the per-exec `stdin_inflight` semaphore, exactly one permit, - before comparing or writing a non-empty chunk. A second concurrent - `WriteStdin` for the same exec either waits until its - `client_deadline_ms`/`stdin_write_deadline_ms` expires or returns - `stdin-backpressure`. This serializes offset advancement and prevents - same-exec fan-in from building more than the single bounded stdin queue. -5. After both permits are held, validate `data.len() <= - effective_limits.max_chunk_bytes`. An effective-limit `max + 1` - request that is still below the ttRPC frame cap is rejected with - `max-chunk-exceeded`; it may have incurred one bounded decoded bytes - allocation, but it is never copied into session storage or queued - behind another stdin write. - -Therefore, for `N` active connections, malicious concurrent -`WriteStdin` fan-in is bounded by: - -```text -decoded request bytes <= N * 4 * 4 MiB -session-copied payload bytes <= N * 4 * 1 MiB -per-exec guestd-retained stdin <= 1 decoded in-flight chunk + 1 bounded queued chunk -``` - -The per-VM connection cap from the guest-control transport budget bounds -`N`; with the initial four-connection cap, worst-case decoded stdin -pressure is 64 MiB per VM before ordinary Rust allocator overhead, while -session-copied stdin is bounded to 16 MiB per VM before per-exec queue -limits. This budget is separate from output log retention and must be -included in RSS evidence. - -Overload behavior is fail-closed and non-spawning: capacity failures -return typed errors before creating a process, allocating a retained log, -or accepting another long-poll waiter. Error payloads carry only bounded -limit metadata and never include argv, environment, output bytes, socket -paths, token material, MACs, or guest free-form strings. - -## Server-side storage model - -Each exec owns two output log objects, or one stdout log for TTY mode. -Each log is an append-only byte sequence with a bounded retained window. -The implementation may use a ring buffer, segmented files under guestd's -runtime directory, or a hybrid, but the visible semantics are identical: -absolute offsets, immutable retained bytes, explicit dropped-byte counts, -and EOF markers. - -Attached execs keep live logs at least until the process reaches a -terminal state and all attached readers have consumed EOF. Detached execs -persist logs until the retention TTL expires for a terminal record, the -operator explicitly removes the exec record, or the VM reboots. - -Detached execs survive a guestd restart **within one boot**: guestd -persists each exec's lifecycle record and reconciles (re-adopts) live -transient units on startup, so a host caller can re-discover and continue -operating on a detached exec by id after a guestd restart. A boot-id -mismatch is rejected as `stale-session`; a record evicted by retention is -reported as `exec-expired`. Attached execs are unchanged: their live -runtime state does not survive a guestd restart, and a host caller observes -`stale-session` rather than reattaching to a reused identity. - -### Retained-log storage security - -Retained stdout/stderr bytes are **guest-local state**. The host may -receive them only as the explicit response body of -`ReadOutput`/`ExecLogs`, and the CLI may write them only to the -operator-requested terminal, stdout/stderr, or output file. They must -not be copied into host daemon state, broker audit records, host metrics, -host spans, host health JSON, bundle manifests, or any host-visible -sidecar directory. - -### Guest-control audit contract - -Guest-control audit records are allowlist-only. They may contain: - -- bounded operation kind from the closed guest-control operation enum: `hello`, - `authenticate`, `capabilities`, `health`, `exec-create`, `exec-inspect`, `exec-wait`, - `exec-logs`, `exec-list`, `write-stdin`, `read-output`, - `close-stdin`, `tty-win-resize`, `exec-signal`, `exec-cancel`, and - `framework-guest-op`; -- VM/environment identifiers, caller role/uid where already authorized, - target uid/user kind, and bounded capability names; -- bounded outcome/error/remediation enums; -- numeric counters and limits such as byte counts, offsets, chunk counts, - durations, retry-after, and truncation booleans; -- timestamps and monotonic state-generation numbers. - -No other audit operation kind is valid until a schema update and security -review add it to the closed enum. - -They must not contain argv, cwd, environment values, stdout/stderr/stdin -payload bytes, retained log paths, tokens, MACs, transcripts, CH/vsock -socket paths, guest-derived free-form errors, or unbounded request/session -IDs unless a later ADR explicitly approves a specific bounded identifier -shape. Tests must seed canaries for every denied class and fail if any -daemon, broker, guestd, userd, metric, span, health, or JSON error surface -contains them outside the explicit stdio/log payload APIs. - -If retained logs are file-backed, they live below guestd-owned guest -paths, never below `/nix/store`, a host-shared mount, or a virtiofs export: - -- live attached state: `/run/d2b/guest-control/exec///`; -- detached retained state: `/run/d2b-exec/slot-/` - a root-owned, - `0700`, boot-scoped slot-keyed directory. Detached state is **not** - per-user and **not** keyed by the opaque exec id: it is keyed by the - fixed slot index, and is owned by the root guestd service, not a target - workload user or `d2b-userd`. - -The path components above are design-level names; implementations may use -different leaves only if they preserve the same security properties: - -1. The top-level runtime/state parents are created by guest activation (or - a tmpfiles rule) as root-owned, mode `0700`, boot-scoped. Per-exec - directories and log segments are `0700` directories and `0600` files, - owned by the root `d2b-guestd` service account, not by the target - workload user. -2. Detached retained-log ownership is served by the root guestd and its - trusted runner; there is no `d2b-userd` involvement and no per-user - retained-log path to resolve. Attached exec and the detached workload - child both run as the host-fixed workload user (`ssh.user`) inside a PAM - login session, never root; the wire `user` field is ignored and there is - no separate user-session daemon (`d2b-userd` was removed; see - [ADR 0030](../adr/0030-guest-exec-as-workload-user.md)). -3. Every open is rooted at the pre-opened runtime/state directory and uses - symlink-safe, beneath-root traversal. Symlinks, `..`, hard-link count - surprises, non-regular log segments, world/group-writable parents, and - cross-device escapes are fatal `retained-log-path-unsafe` errors before - bytes are read or written. -4. File-backed segments are created with exclusive create semantics and - restrictive mode before any child output is accepted. Cleanup must - unlink by directory file descriptor, not by re-parsing a string path. -5. Detached quotas are exact and VM-global: each stream is capped at - **4 MiB**, each exec retains **2 streams**, and the VM-global retained-log - budget is **256 MiB** (32 retained slots × 2 streams × 4 MiB). Concurrency - is bounded to **8 active** execs and **32 retained** slots per VM. - `ExecCreate(detached=true)` fails with a bounded - `retained-log-quota-exceeded` error if admitting the exec would exceed - the active, retained-slot, or log-byte budget; the - `quota == slots × 2 × stream_cap` invariant is asserted. -6. Cleanup is deterministic. Attached non-detached logs are removed after - all readers have observed EOF, or after a short terminal-state grace - period if the client disappears. A detached **terminal** record expires - at the earlier of explicit operator removal, VM reboot, or the retention - TTL; the default TTL is **30 minutes** and applies to terminal records - only - a detached `Running` job runs **indefinitely** and is never - reaped by TTL/GC. Startup reconciliation removes expired, orphaned, - partially-created, and path-unsafe records before serving `ExecLogs`. - -Guestd may keep small live rings in memory instead of files for attached -execs, but detached retained logs are file-backed and count against the -exact VM-global quota above. In-memory attached implementations must prove -that restart loss is surfaced as `stale-session` rather than silently -serving partial output as durable retained logs. - -Output reader tasks append bytes from the child fd into the log only -while the log has capacity. When the retained window is full: - -- For attached, non-detached execs, the reader stops reading from the fd, - causing pipe/PTY backpressure to reach the child. If the full condition - persists past `slow_consumer_grace`, guestd cancels the exec with - `slow-consumer-cancelled`. -- For detached execs, the log may evict the oldest bytes and increment - `dropped_bytes` up to the configured retention limit. The child is not - cancelled solely because no host is attached, but total disk/memory use - remains bounded. -- For attached+detached execs, the detached retention policy wins for log - storage, while attached clients still receive `offset-expired` if they - fall behind the retained window. - -## Polling and long-poll behavior - -Long-polling is deliberately short. It hides idle polling latency without -allowing one stalled unary call to monopolize resources indefinitely. -Recommended CLI loop: - -1. Issue parallel `ReadOutput(stream=stdout, wait=true)` and, for non-TTY, - `ReadOutput(stream=stderr, wait=true)` from current cursors. -2. Issue `ExecWait(timeout_ms=long_poll_timeout)` concurrently or after a - read timeout. -3. Write local terminal/stdout/stderr bytes immediately as chunks arrive. -4. Re-issue reads until each stream reports EOF and wait reports a - terminal state. - -The server enforces a per-exec cap on simultaneous pending read waits per -stream, normally one per connection, plus the per-VM caps above. A second -wait at the same cursor replaces or rejects the older one with -`superseded-read-wait`; per-VM exhaustion returns -`read-wait-capacity-exceeded`. This prevents abandoned clients from -accumulating waiters. - -## Ordering of resize, signal, cancel, and exit - -Every control mutation carries a `request_id` and a `control_seq` -supplied by the client. A new control request is accepted when -`control_seq` is **strictly greater** than `last_control_seq` (gaps are -allowed); a `control_seq` that is not strictly increasing returns -`control-seq-mismatch` with the expected lower bound. Accepted controls are -idempotent only for the same retained `request_id`, `control_seq`, and -payload. Reusing a retained `request_id` with a different sequence or -payload returns `request-id-conflict`. This sequence covers: - -- `TtyWinResize`; -- `ExecSignal`; -- `ExecCancel`. - -`WriteStdin` and `CloseStdin` are ordered by stdin byte offset and -`request_id`, not by `control_seq`. -Output chunks are ordered by stream offset. Terminal state is ordered -after all output bytes guestd read before observing process exit. If a -process exits while unread bytes remain in OS pipes, guestd drains those -pipes until EOF or the bounded buffer policy blocks; only then does it -return stream EOF. If draining cannot complete because a client is too -slow, the terminal state becomes `slow-consumer-cancelled` rather than -silently dropping bytes for attached sessions. - -### Resize - -`TtyWinResize` is valid only for TTY execs and includes `{rows, cols}`. -The first resize after create must have `control_seq = 1` only if the CLI -observes a size change after `ExecCreate`; otherwise the create-time -geometry is authoritative. Guestd applies the PTY resize with the PTY -backend's `TIOCSWINSZ` equivalent and relies on the kernel to deliver -SIGWINCH to the current foreground process group. Guestd must not also -call `killpg(SIGWINCH)` for the ordinary resize path, because that would -duplicate the kernel notification. If the requested rows/cols equal the -last applied size, guestd acknowledges the idempotent request without -issuing another `TIOCSWINSZ`; intentional duplicate SIGWINCH would -require a future explicit API flag. - -### Signal - -`ExecSignal` includes a numeric signal and a `target` enum: -`foreground_process_group` for TTY and `process_tree` for non-TTY. In the -current implementation only `foreground_process_group` is accepted: guestd -rejects any other target (`process_tree`, unspecified, or unknown) with -`protocol-error` *before* the control sequence is consumed, so the client may -retry with a valid target at the same seq. Non-TTY `process_tree` signalling is -deferred; interactive TTY exec is foreground-process-group only. The signal -number is matched against the frozen TTY allowlist (`HUP`, `INT`, `QUIT`, -`KILL`, `USR1`, `USR2`, `TERM`, `CONT`, `TSTP`, `WINCH`); any other number is -rejected with `protocol-error`, also before the seq is consumed. A signal -accepted after process exit is idempotent no-op only for duplicate -`request_id`; otherwise it returns `exec-already-exited`. - -For TTY execs, guestd resolves `foreground_process_group` from the PTY -foreground owner (`tcgetpgrp` semantics), not from the root shell PID. -This matters after an interactive shell transfers the controlling -terminal to a foreground child job: Ctrl-C and `ExecSignal(INT)` must -hit that child process group, then allow the shell to regain the terminal -and report the child's signal-derived status. - -### Cancel - -`ExecCancel` is the host-initiated cleanup primitive. It closes stdin, -terminates the process according to the guest policy escalation, marks -state `cancelled`, wakes all waiters/readers, and preserves retained logs -according to detached/attached retention. Transport disconnect does not -cancel detached execs. It cancels attached non-detached execs after a -small grace period unless another authorized attach takes ownership. - -Attached **TTY** execs are non-durable and not re-ownable: on caller -disconnect (or cancel) guestd tears the session down by dropping all -PTY-master-owning tasks (output reader, stdin writer, control), which -closes the master and delivers `SIGHUP` to the foreground process group, -then escalates to `SIGKILL` after the grace window and reaps the session. -A TTY session's identity does not survive disconnect; a later attach -cannot resume it. Non-TTY attached re-ownership above does not apply to -TTY execs. - -## EOF, close, and half-close semantics - -- Stdin close and output EOF are independent. -- `CloseStdin` never implies process termination. -- Child exit never implies stdin close succeeded; inspect reports both. -- Output EOF is per stream. In non-TTY mode stdout may EOF before stderr. -- In TTY mode stderr is absent; all PTY output is stdout. -- PTY EOF is a drained state, not the first kernel hangup indication: - bytes returned before Linux `EIO` or `POLLHUP` remain part of stdout - and advance `stdout_end_offset` before EOF becomes visible. -- Ctrl-D is data in TTY mode, not a protocol close. The CLI sends byte - `0x04` through `WriteStdin` when the local terminal produces it. -- EOF responses are replayable forever while the exec record exists. - -## Bounded memory and backpressure proof - -Raw ttRPC streams failed the feasibility gate because application receivers could stall -while payloads accumulated behind stream delivery tasks. This design -moves flow control into the application protocol: - -1. The server never accepts more than one bounded stdin chunk per exec - beyond the decoded in-flight request. A blocked child therefore fills - that bounded queue and blocks or rejects the next `WriteStdin`; guestd - does not build an unbounded stdin queue. Partial Linux pipe/PTY writes - are completed from that queue before any later stdin offset is admitted. -2. Output is copied from child fds only into bounded logs. If an attached - reader stops, output log capacity fills, guestd stops reading, and the - kernel pipe/PTY blocks the child. If the condition lasts past the - grace window, guestd returns a typed slow-consumer cancellation. -3. Detached output uses bounded ring retention. Dropping old detached log - bytes is explicit through `start_offset`, `dropped_bytes`, and - `truncated` fields, never silent for attached readers. -4. ttRPC rejects messages above its fixed 4 MiB frame cap before handler - entry. Effective `max_chunk_bytes` violations that fit under that - transport cap may allocate one decoded protobuf `bytes` field, but - per-connection decoded-byte semaphores and per-exec stdin permits bound - the allocation and reject before session-buffer copy. -5. Pending waits are bounded per exec and timeout quickly. -6. Per-connection and per-VM caps limit concurrent exec count, pending RPC - count, total retained log bytes, and total live buffer bytes. - -With default limits, a non-TTY attached exec consumes at most roughly: - -```text -stdin_rpc_chunk <= 64 KiB -decoded_stdin_budget <= 16 MiB per connection, shared across execs -stdout_live_buffer <= 1 MiB -stderr_live_buffer <= 1 MiB -read_response_chunks <= 2 * 64 KiB -metadata/dedupe/events <= implementation budget, target < 1 MiB -``` - -Detached execs add configured retained log storage but remain bounded by -`detached stdout log + detached stderr log`. The conformance budget of -64 MiB above idle for four concurrent sessions is therefore achievable -with the defaults and must be proven by tests before implementation -locks. - -## Conformance matrix mapping - -| ADR 0028 row | Chunked stdio behavior | -| --- | --- | -| stdin open/close, EOF, TTY Ctrl-D | `stdin_open`, `WriteStdin` offsets, `CloseStdin`, and TTY Ctrl-D-as-data define this explicitly. | -| stdout/stderr separation | Separate logs and read RPCs in non-TTY mode. | -| TTY merge | PTY output maps to stdout; stderr read is typed unavailable. | -| initial geometry and resize ordering | Geometry is part of create; later resizes use `control_seq`. | -| PTY leadership / foreground process group | Not solved by wire format; implementation must use the safe-PTY proof path: session leader, controlling terminal, `tcgetpgrp` foreground owner, and child job handoff are required. | -| Ctrl-C/signal delivery | `ExecSignal` targets the current `tcgetpgrp` foreground process group for TTY, including after a shell hands the terminal to a child job. | -| exit code/signal propagation | `ExecWait` and `Inspect` hide recorded terminal state until output preceding terminal observation is retained, delivered/acknowledged, or explicitly dropped with cursor accounting, then expose `visible_terminal_status`. | -| bounded memory / backpressure | Bounded logs, one stdin chunk, limited waiters, and slow-consumer cancellation. | -| concurrent sessions/fairness | Per-exec caps and short polling prevent one stalled exec from owning global queues. | -| cancellation/disconnect cleanup | `ExecCancel` plus attached/detached disconnect policy. | -| half-close behavior | `CloseStdin` is independent of output reads and wait. | -| guestd restart / VM reboot | Detached execs are re-adopted within one boot via persisted records + transient-unit reconciliation; a boot-id mismatch is `stale-session` and a retention-evicted record is `exec-expired`. Attached session identity does not survive a restart and rejects unsafe reattach. | -| raw-mode restoration | CLI owns local raw mode and restores on terminal state or protocol error. | -| max message size | ttRPC fixed 4 MiB frame cap plus bounded post-decode `max_chunk_bytes` check. | -| malformed messages | Typed `protocol-error` codes with bounded fields only. | -| no data leakage | Logs/metrics carry counters, offsets, outcomes, and booleans, not payloads. | - -## Health status model - -Guest-control health is bounded and schema-versioned. Guest-returned -`Health` payloads carry `origin = guest-reported`; host-synthesized readiness -failures that happen before a guest `Health` RPC can complete carry -`origin = host-synthesized`. Every health status then includes a state enum -plus bounded reason/remediation enums: - -- `healthy` -- `degraded` -- `unavailable-old-generation` -- `listener-absent` -- `transport-unreachable` -- `auth-failed` -- `protocol-mismatch` -- `stale-session` - -The bounded reason enum is closed for this protocol: - -- `none` -- `old-generation` -- `listener-absent` -- `connect-refused` -- `connect-timeout` -- `eof-before-ack` -- `malformed-ack` -- `ack-too-long` -- `transport-io` -- `auth-token-rejected` -- `protocol-version-unsupported` -- `session-generation-mismatch` -- `exec-subsystem-unavailable` -- `log-storage-unavailable` -- `quota-exceeded` -- `rate-limited` -- `internal-health-check-failed` - -The bounded remediation enum is also closed: - -- `none` -- `retry` -- `restart-vm` -- `upgrade-guest` -- `check-auth-token` -- `check-guestd-service` -- `reduce-load` -- `inspect-guest-logs` - -Allowed state mappings: - -| State | Allowed reasons | Allowed remediations | -| --- | --- | --- | -| `healthy` | `none` | `none` | -| `degraded` | `exec-subsystem-unavailable`, `log-storage-unavailable`, `quota-exceeded`, `rate-limited`, `internal-health-check-failed` | `retry`, `reduce-load`, `inspect-guest-logs`, `restart-vm` | -| `unavailable-old-generation` | `old-generation` | `upgrade-guest`, `restart-vm` | -| `listener-absent` | `listener-absent` | `check-guestd-service`, `restart-vm` | -| `transport-unreachable` | `connect-refused`, `connect-timeout`, `eof-before-ack`, `malformed-ack`, `ack-too-long`, `transport-io` | `retry`, `restart-vm`, `check-guestd-service` | -| `auth-failed` | `auth-token-rejected` | `check-auth-token` | -| `protocol-mismatch` | `protocol-version-unsupported` | `upgrade-guest` | -| `stale-session` | `session-generation-mismatch` | `retry`, `restart-vm` | - -The matrix is also origin-constrained: `healthy` and `degraded` are -guest-reported, while `unavailable-old-generation`, `listener-absent`, -`transport-unreachable`, `auth-failed`, `protocol-mismatch`, and -`stale-session` are host-synthesized readiness/status results. - -`healthy` requires CH `CONNECT`, Hello challenge, Authenticate, and Health to -succeed on the same post-CONNECT stream. `degraded` means guestd is -authenticated and serving Health but one bounded subsystem check failed; -callers may proceed only with operations whose capability bit remains healthy. Every other -state is unavailable for new exec work. Health, status JSON, logs, -metrics, spans, and audit records must carry only the bounded state, -reason, remediation, protocol version, and capability names; they must not -include socket paths, token/MAC material, transcripts, argv/env/cwd, -payload bytes, or guest free-form error text. - -## CLI behavior - -> **Scope note.** The guest-control exec *RPC/service* surface -> (`ExecCreate`/`WriteStdin`/`CloseStdin`/`ReadOutput`/`TtyWinResize`/ -> `ExecSignal`/`ExecInspect`/`ExecWait`/`ExecCancel`) is the shipped -> contract. The typed `d2b exec` CLI described below provides -> `run`, `attach`, `wait`, `status`, `list`, `logs`, and `kill` -> commands. A run targets a Guest ResourceRef, -> and management targets use EphemeralProcess ResourceRefs. Detached exec -> uses -> the same never-root workload-user model as attached exec, remains inside -> guestd (no broker op), and is advertised only when guestd has reconciled -> a usable detached registry. - -### Attached exec - -`d2b exec run Guest/ -- ` creates a non-TTY exec with stdin -closed. The CLI reads stdout/stderr through offsets until both streams -EOF and `ExecWait` returns terminal. The CLI exits with the remote exit -code for normal command exit. Remote signal termination is reported as the -command result with signal metadata and shell-style status `128 + signal`. -Typed d2b errors are reserved for transport, protocol, authorization, -and pre-exec failures. - -`--interactive` (`-i`) opens stdin and must be paired with `--tty` -(`-t`): the guest-control transport forwards stdin only in PTY mode, so -`-i` without `-t` is rejected with a usage envelope. With `-it`, the CLI -forwards local stdin in -`max_chunk_bytes` chunks with increasing offsets and sends `CloseStdin` -on local EOF. If `WriteStdin` returns `stdin-backpressure`, the CLI -blocks local input reading or uses terminal flow control until retry is -accepted. - -`--tty` allocates a PTY, sends initial geometry in `ExecCreate`, sends -resizes as sequenced `TtyWinResize` calls with rows/cols in `1..65535`, -merges output through stdout, and restores local terminal raw mode on every -return path. - -### Detached exec - -`d2b exec run Guest/ -- ` creates a non-TTY exec and -returns an opaque `exec_id` plus the initial state. The command runs as the -VM's workload user inside a PAM login session, never as root, and -continues after the host CLI disconnects. `-d` is mutually exclusive with -`-i` and `-t`, and the command form still requires `--`; bare `argv[0]` -names are resolved by the workload user's login `PATH` through the same -`exec "$@"` wrapper used by attached exec. - -Detached management is VM-first so management words remain valid VM names: - -- `d2b exec list Guest/` lists retained process records; -- `d2b exec logs EphemeralProcess/` returns retained stdout/stderr; -- `d2b exec status EphemeralProcess/` returns state and terminal - disposition; -- `d2b exec kill EphemeralProcess/` maps to `ExecCancel`. - -`kill` is a two-phase cancel: guestd requests graceful termination, waits a -bounded grace window, then force-kills the workload if needed. It is -idempotent, returning `cancelling` for a live record or `already-terminal` -for a terminal one. - -Detached logs are bounded ring buffers with dropped/truncated accounting. -`ExecLogs` uses per-stream offsets (`stdout` and `stderr`) so clients can -resume from retained cursors without replaying already-consumed bytes. - -Guestd reconciles detached state before advertising the detached -capability: it re-adopts structurally valid runner/workload units, cleans -orphaned workload units, and starts a periodic reaper that expires terminal -records and releases retained-log slots. Detached exec remains entirely -inside guestd's exec service; there is no privileged broker operation. - -### Old-generation VMs - -As required by ADR 0028, new exec commands do not fall back to SSH. If a -running VM lacks guest-control capabilities, the CLI returns -`guest-control-unavailable-old-generation` with remediation. Existing -SSH-backed compatibility commands outside generic exec keep their -separate compatibility window. - -### Logs UX - -`ExecLogs` uses the same offset model as `ReadOutput(stream=...)` but -packages stream records for CLI display or JSON. Human logs default to -available retained bytes and warn when `dropped_bytes > 0`. JSON includes -`startOffset`, `endOffset`, per-stream `stdoutNextOffset` / -`stderrNextOffset` and `stdoutEof` / `stderrEof`, `droppedBytes`, and -`truncated`, but never embeds unbounded metadata. Payload bytes are -emitted only as the requested command output/log stream, not in error -objects, daemon logs, metrics, or health JSON. - -## Errors - -Required typed error kinds: - -- `exec-not-found` -- `exec-already-exited` -- `exec-expired` -- `stale-session` -- `tty-required` -- `tty-stderr-unavailable` -- `stdin-not-open` -- `stdin-closed` -- `stdin-closed-by-process` -- `stdin-offset-mismatch` -- `stdin-backpressure` -- `stdin-byte-budget-exhausted` -- `offset-expired` -- `offset-in-future` -- `offset-exhausted` -- `output-lost` -- `max-chunk-exceeded` -- `control-seq-mismatch` -- `request-id-conflict` -- `superseded-read-wait` -- `read-wait-capacity-exceeded` -- `wait-capacity-exceeded` -- `exec-capacity-exceeded` -- `exec-attach-capacity-exceeded` -- `guest-exec-disabled` -- `guest-exec-root-denied` -- `guest-exec-user-denied` -- `cwd-invalid` -- `cwd-denied` -- `retained-log-path-unsafe` -- `retained-log-quota-exceeded` -- `rate-limited` -- `slow-consumer-cancelled` -- `guest-control-unavailable-old-generation` -- `auth-failed` -- `transport-unreachable` -- `protocol-error` - -Each error response carries only bounded fields: expected offsets, -current windows, limits, state enum, and remediation enum. It must not -include argv, env, cwd, stdout/stderr bytes, token material, socket -paths, or guest-derived free-form strings. - -## Observability - -Metrics and logs may include: - -- bytes read/written per stream; -- chunks read/written per stream; -- read wait timeouts; -- blocked stdin write duration histograms; -- output backpressure duration histograms; -- cancellations by bounded reason enum; -- offset-expired and max-chunk-exceeded counters; -- current exec count and retained-log byte gauges. - -They must not include payload bytes, session IDs, command lines, -environment variables, cwd, credential paths, CH socket paths, HMAC -material, or guest free-form errors. - -Health responses and CLI JSON use the same rule except for fields that are -the explicit user-facing API result. The interactive `-i`/`-t` exec forms -are human-only and reject `--json` with a usage envelope, while the -non-TTY `d2b exec run Guest/ --json -- ` returns a single -terminal envelope (exit code + source/reason + bounded captured output). -On the service surface, `ExecInspect`/`ExecLogs` may return the -`execId`/cursor state or the requested log payload when the caller asked -for it, but daemon logs, metrics, spans, health JSON, and error JSON must -not duplicate those payloads or IDs. - -## Required tests - -Before implementation exits design hardening, add at least: - -1. protobuf/schema drift tests for every message above; -2. receive-limit tests: ttRPC's fixed 4 MiB frame cap rejects cap+1 - messages before handler entry; effective `max_chunk_bytes + 1` below - that transport cap fails with one bounded decoded allocation and no - session-buffer copy; -3. stdin offset/idempotency tests, including duplicate request IDs and - mismatched duplicate payloads; -4. close-stdin and `WriteStdin.close_after` tests for non-TTY pipe and - TTY Ctrl-D-as-data behavior, plus TTY protocol-side close proving the - PTY master/writer stays open and output after close is not lost; - duplicate close/final-write is accepted only for the same retained - `request_id` and payload/final offset, while mismatched or - different-request duplicates fail typed; -5. stdout/stderr byte-exact 64 MiB + 64 MiB non-TTY test; -6. stdin 16 MiB slow-reader test with bounded RSS; -6a. malicious concurrent `WriteStdin` fan-in test: four hard-maximum-sized - ttRPC-frame requests on one connection consume the 16 MiB decoded-byte - budget, a fifth concurrent request receives - `stdin-byte-budget-exhausted` or waits for a permit, and concurrent - effective-limit `max + 1` writes to one exec preserve offset order and - never queue more than one chunk behind the single per-exec stdin - permit; include pipe and PTY partial child-write cases proving the - bounded queue drains without duplicate or lost stdin bytes before - `CloseStdin` is observed; -7. deterministic slow stdout/stderr consumer stress proving block or - `slow-consumer-cancelled` with bounded retained bytes; a separate - non-default runtime soak may cover 30-second wall-clock behavior; -8. detached retention tests proving dropped-byte accounting and - `offset-expired` behavior; -9. long-poll timeout and waiter-cap tests; -10. resize ordering tests that fail on reordered `control_seq`, prove a - single `TIOCSWINSZ` yields a single SIGWINCH, and prove unchanged - sizes are deduplicated before `TIOCSWINSZ`; control tests must also - cover `request_id` idempotent replay for resize, signal, and cancel, - and reject mismatched duplicate control payloads; -11. signal ordering and foreground process-group delivery tests covering - session leadership, controlling-terminal setup, `tcgetpgrp` - ownership, and SIGINT after a shell hands the terminal to a - foreground child group; -12. concurrent-session fake-scheduler test with slow-output, - blocked-stdin, interactive TTY, and unary health loop, requiring - bounded service-turn gaps and no byte-skew starvation; -13. guestd restart and VM reboot stale-session rejection tests; -14. CLI raw-mode restoration tests for success, signal, protocol error, - and disconnect; -14a. Health state/reason/remediation matrix tests enumerating every - Health state, reason, and remediation, rejecting invalid combinations - such as `healthy` plus an error reason, and proving CONNECT, - Hello challenge, Authenticate, and Health failures map to the documented - bounded states - without leaking socket paths, tokens, transcripts, guest text, or - unbounded IDs; -15. retained-log storage security tests covering guest-local path roots, - ownership/mode, symlink and hard-link rejection, attached per-user - isolation, and the detached exact VM-global quota enforcement (8 active / - 32 retained slots × 2 streams × 4 MiB = 256 MiB; detached state is - slot-keyed and **not** per-user), TTL/startup cleanup, and the - absence of retained stdout/stderr bytes from host-visible state other - than explicit `ExecLogs` responses; -16. observability and audit redaction tests proving stdout/stderr/env/ - argv/cwd/token/socket/transcript material never enters logs, broker - or guest-control audit records, metrics, spans, health, or JSON - errors. -17. PTY close/drain tests proving Linux `EIO` or `POLLHUP` after slave - close becomes stdout EOF only after all buffered output has advanced - the stream cursor. - -### Redaction test matrix - -The implementation test suite must seed every surface below with stable -canary values and assert that only the allowed API response contains them. -Use distinct canaries for argv, cwd, environment, credential paths, HMAC -MACs, socket paths, IDs, guest error text, stdout, stderr, and transcripts -so a failure names the leaking class. - -| Canary class | Seed location | Must be absent from | Allowed location | -| --- | --- | --- | --- | -| argv / command line | `ExecCreate argv = ["true", "ARG_CANARY"]` | daemon logs, guestd logs except bounded kind, audit records, metrics labels/samples, spans, health, error JSON | never; CLI human dry-run is out of scope for guestd telemetry | -| cwd | `--workdir /home/alice/CWD_CANARY` | logs, audit records, metrics, spans, health, error JSON | never; use bounded `cwd-invalid`/`cwd-denied` kinds | -| env / tokens | `--env TOKEN_CANARY=...`, env-file path | logs, audit records, metrics, spans, health, error JSON, retained-log metadata | child stdout/stderr only if the command itself prints it and user requested logs | -| credential path | guestd `LoadCredential` path and token-file validation errors | logs, audit records, metrics, spans, health, CLI JSON errors | never; report bounded credential error kind | -| HMAC/MAC/transcript | failed auth proof with known MAC and transcript canaries | logs, audit records, metrics, spans, health, CLI JSON errors | never; report bounded auth failure kind | -| CH/vsock/socket paths | stale or refused CONNECT using a path canary | logs, audit records, metrics, spans, health, CLI JSON errors | never; report bounded transport kind | -| session / exec / request IDs in telemetry | successful and failed calls with ID canaries | logs, audit records, metrics labels, span attrs/events, health | CLI JSON fields whose contract explicitly returns `execId`/cursor state | -| guest-derived free-form errors | child exits after writing `ERR_CANARY` to stderr; guestd returns malformed free-form text in a fake transport | daemon logs, guestd structured logs, audit records, metrics, spans, health, CLI JSON error object | stderr stream only when requested as command output/logs | -| stdout/stderr payloads | child writes `STDOUT_CANARY` and `STDERR_CANARY` | logs, audit records, metrics, spans, health, CLI JSON errors, inspect/wait JSON | `ReadOutput`/`ExecLogs` payloads and attached CLI stdout/stderr | -| socket paths / transcripts in debug formatting | force `Debug`/`Display` on transport/auth errors | all structured logs, audit records, spans, health, CLI JSON errors | never | - -Required assertions: - -1. Capture daemon, guestd, and userd structured logs plus host broker and - guest-control audit records during the test and grep for every canary; - matches are allowed only in the explicit payload stream under test. -2. Scrape Prometheus/OpenTelemetry metric output and assert canaries are - absent from metric names, labels, exemplars, and sample values except - numeric byte counts. -3. Export spans/events and assert canaries are absent from span names, - attributes, events, status descriptions, and exception fields. -4. Call `Health`, `ExecInspect`, `ExecWait`, failed `ExecCreate`, failed - auth, and failed `ExecLogs --json`; assert JSON contains bounded enums, - counters, offsets, booleans, and remediation only. -5. Run the same canaries through attached, detached, TTY, non-TTY, - success, protocol-error, auth-error, quota-error, and stale-session - paths so redaction is not limited to the happy path. - -## UX latency tradeoffs - -Chunked stdio trades stream immediacy for explicit bounds. Interactive -latency depends on read long-poll timeout, chunk size, scheduler fairness, -and RPC overhead. The defaults intentionally choose small 64 KiB chunks -and 100 ms read waits so shell echo and resize feedback stay responsive -under load. Larger chunks improve bulk throughput but can increase -head-of-line time for a single response; callers may request smaller -chunks for TTY sessions. - -The CLI should issue stdout/stderr reads concurrently and immediately -re-poll after any response. It should not sleep between successful reads. -For bulk detached logs, the CLI may request larger chunks up to the -effective maximum. - -## Implementation complexity - -This design is more complex than raw ttRPC streams because guestd must -own per-exec logs, offsets, dedupe state, waiter accounting, and explicit -backpressure policy. It is still simpler than a custom binary stream -because: - -- lifecycle and I/O remain protobuf/ttRPC contracts; -- every method is unary and fits existing ttRPC request handling; -- retries/idempotency are explicit; -- attached and detached logs share one cursor model; -- message-size enforcement lives at protobuf RPC boundaries; -- conformance failures can be isolated to individual RPCs. - -The main implementation risks are fairness bugs in the guestd scheduler, -PTY drain behavior after process exit, retention-window edge cases, and -incorrect CLI retry behavior around `stdin-backpressure` or -`offset-expired`. - -## Implementation gates - -- PTY output backpressure is kernel- and workload-sensitive. The feasibility - proof covers the wire/protocol model for TTY stdout-only reads and bounded - output logs; - the implementation must add runtime tests proving the selected PTY crate - and guest kernel block writers instead of dropping data before - guest-control ships. -- Detached logs can hide slow consumers by evicting old bytes; UX must - make truncation obvious in human and JSON output. -- Short long-poll timeouts increase RPC rate. Capability negotiation may - need per-VM rate limits if many clients attach concurrently. -- Server-side segmented files reduce RSS but introduce cleanup and disk - quota risk. Pure memory rings are simpler but constrain detached log - sizes. Implementation must choose one and test quota behavior. -- `control_seq` protects ordering for host-originated controls, but - process exit can race with accepted signal/resize calls. The state - machine must define the visible result and tests must lock it. -- Guestd restart cannot preserve live process handles unless a later - design adds durable supervision. Current design intentionally rejects - stale reattach rather than pretending continuity. - -## References - -- [ADR 0028: Guest control plane over virtio-vsock](../adr/0028-guest-control-plane-over-vsock.md) -- [ADR 0030: Guest exec runs as the workload user in a PAM login session](../adr/0030-guest-exec-as-workload-user.md) -- [ADR 0031: Bare commands and detached workload-user exec](../adr/0031-bare-command-and-detached-exec.md) -- [Guest control feasibility dossier](../adr/guest-control-feasibility-dossier.md) -- [Kata Agent protocol stdio RPCs](https://github.com/kata-containers/kata-containers/blob/6d2066b692ce69a908bb4daec2c6b71ccfad3829/src/libs/protocols/protos/agent.proto#L33-L49) -- [Kata Agent stream message shapes](https://github.com/kata-containers/kata-containers/blob/6d2066b692ce69a908bb4daec2c6b71ccfad3829/src/libs/protocols/protos/agent.proto#L211-L227) diff --git a/docs/reference/guest-control-exec-io-credit-window.md b/docs/reference/guest-control-exec-io-credit-window.md deleted file mode 100644 index 5b4d246f3a..0000000000 --- a/docs/reference/guest-control-exec-io-credit-window.md +++ /dev/null @@ -1,407 +0,0 @@ -# Guest-control exec I/O credit-window protocol - -This document records the bounded credit-window ttRPC stream candidate -for d2b guest-control exec I/O. The feasibility decision selected -[chunked stdio RPCs](./guest-control-exec-io-chunked-stdio.md) instead -because raw ttRPC stream buffering was the observed failure mode and the -unary chunked model is simpler to bound and test. This credit-window -overlay remains a fallback candidate if the selected chunked-stdio path -fails later implementation evidence. - -Under this candidate, unary guest-control APIs continue to use ordinary -ttRPC request/response messages. Attached and detached exec I/O uses a -ttRPC async duplex stream carrying d2b `TerminalFrame` protobuf -messages with application-level byte credit. Raw ttRPC stream -backpressure is not relied on for correctness. - -The design goal is Docker-like exec behavior while preserving bounded -memory, byte-exact stdout/stderr/stdin delivery, explicit half-close -semantics, and typed failure when either endpoint stops consuming. - -## Stream model - -`ExecCreate`, `ExecInspect`, `ExecLogs`, `ExecSignal`, and `ExecCancel` -are unary control RPCs. `ExecAttach` is the only duplex streaming RPC. -A host may attach when creating an exec, or attach later to a detached -exec that is still running or has retained logs. - -```protobuf -syntax = "proto3"; - -package d2b.guestcontrol.v1; - -service GuestControl { - rpc ExecAttach(stream TerminalFrame) returns (stream TerminalFrame); -} - -message ExecSessionId { - string vm = 1; // bounded VM name, not a secret - bytes exec_id = 2; // 128-bit random ID, encoded as 16 bytes - uint64 generation = 3; // rejects stale sessions after guestd restart -} - -enum TerminalChannel { - TERMINAL_CHANNEL_UNSPECIFIED = 0; - TERMINAL_CHANNEL_STDIN = 1; // host -> guest data - TERMINAL_CHANNEL_STDOUT = 2; // guest -> host data, non-TTY only - TERMINAL_CHANNEL_STDERR = 3; // guest -> host data, non-TTY only - TERMINAL_CHANNEL_TTY = 4; // merged PTY data in TTY mode - TERMINAL_CHANNEL_CONTROL = 5; // resize, signal, exit, errors, credit -} - -enum CloseReason { - CLOSE_REASON_UNSPECIFIED = 0; - CLOSE_REASON_EOF = 1; // clean half-close for a data channel - CLOSE_REASON_CANCELLED = 2; // local cancellation or host disconnect - CLOSE_REASON_SLOW_CONSUMER = 3; // credit starvation timeout - CLOSE_REASON_PROTOCOL_ERROR = 4; - CLOSE_REASON_PROCESS_EXITED = 5; -} - -enum ErrorCode { - ERROR_CODE_UNSPECIFIED = 0; - ERROR_CODE_PROTOCOL = 1; - ERROR_CODE_OVERSIZE_FRAME = 2; - ERROR_CODE_OUT_OF_ORDER = 3; - ERROR_CODE_UNKNOWN_SESSION = 4; - ERROR_CODE_UNSUPPORTED_CHANNEL = 5; - ERROR_CODE_WINDOW_EXHAUSTED = 6; - ERROR_CODE_SLOW_CONSUMER = 7; - ERROR_CODE_INTERNAL = 8; -} - -message TerminalFrame { - ExecSessionId session = 1; - TerminalChannel channel = 2; - - // Monotonic per sender and per session across all frame kinds. The peer - // rejects gaps, duplicates, and regressions with ERROR_CODE_OUT_OF_ORDER. - uint64 sequence = 3; - - // Monotonic byte offset for DATA frames on the named channel. Non-DATA - // frames set this to zero. The receiver uses it to prove byte-exact order. - uint64 data_offset = 4; - - oneof payload { - DataFrame data = 10; - CreditGrant credit = 11; - CloseFrame close = 12; - ResizeFrame resize = 13; - SignalFrame signal = 14; - ExitFrame exit = 15; - ErrorFrame error = 16; - AttachHello hello = 17; - } -} - -message AttachHello { - uint32 protocol_version = 1; - bool tty = 2; - uint32 rows = 3; - uint32 cols = 4; - uint32 max_frame_bytes = 5; - repeated ChannelWindow initial_windows = 6; -} - -message ChannelWindow { - TerminalChannel channel = 1; - uint64 bytes = 2; -} - -message DataFrame { - bytes bytes = 1; // <= negotiated max_frame_bytes -} - -message CreditGrant { - TerminalChannel channel = 1; - uint64 bytes = 2; - uint64 consumed_offset = 3; // byte offset durably written to downstream sink - uint64 ack_sequence = 4; // highest peer sequence observed before grant -} - -message CloseFrame { - TerminalChannel channel = 1; - CloseReason reason = 2; - string detail = 3; // bounded, no command/output/env content - uint64 final_offset = 4; // expected total bytes for data channels -} - -message ResizeFrame { - uint32 rows = 1; - uint32 cols = 2; -} - -message SignalFrame { - string signal = 1; // canonical name such as INT or TERM - int32 number = 2; // Linux signal number for diagnostics -} - -message ExitFrame { - int32 exit_code = 1; // set when exited normally - string signal = 2; // set when terminated by signal - int32 signal_number = 3; -} - -message ErrorFrame { - ErrorCode code = 1; - string message = 2; // bounded and redacted - CloseReason close_reason = 3; -} -``` - -Only `DataFrame` consumes byte credit. Control frames do not consume data -credit, but each endpoint maintains a small bounded control queue so that -errors, close, resize, signal, and exit cannot be hidden behind bulk data. - -## Negotiated limits - -The first frame from each endpoint is `AttachHello`. The effective limit -for the stream is the minimum of both sides' advertised values and the -bundle policy. - -Recommended initial constants for implementation proofs: - -| Limit | Initial value | Rationale | -| --- | ---: | --- | -| `max_frame_bytes` | 32 KiB | Small enough for fairness; far below ttRPC's message ceiling. | -| stdout/stderr/TTY initial credit | 256 KiB per channel | Covers interactive bursts without hiding slow consumers. | -| stdin initial credit | 64 KiB | Avoids buffering large pasted input into a slow process. | -| credit-update threshold | half the channel window | Avoids one grant per small write. | -| credit-update timer | 10 ms | Keeps interactive echo latency below the target under small writes. | -| per-channel queued DATA | one negotiated window | Keeps memory proportional to advertised credit. | -| per-session queued DATA | sum of channel windows, capped at 1 MiB | Prevents one exec from monopolizing memory. | -| control queue | 64 frames per session | Bounded but large enough for resize bursts and teardown. | -| slow-consumer grace | 30 s without credit progress | Matches the slow-consumer stress duration. | - -The exact constants are policy knobs, but any implementation must expose -them through generated schema or documented bundle metadata before they -become configurable. - -## Flow-control algorithm - -Each endpoint tracks these counters per session and data channel: - -- `send_credit`: bytes the peer has granted but this endpoint has not - yet sent; -- `sent_offset`: next byte offset to send; -- `delivered_offset`: bytes received from the peer and written to the - local downstream sink; -- `grantable_bytes`: bytes eligible for a future `CreditGrant` after - downstream completion; -- `last_credit_progress`: monotonic timestamp of the last credit grant - or downstream write completion. - -### Sending DATA - -1. Split data into chunks no larger than `max_frame_bytes`. -2. Send a chunk only when `send_credit >= chunk_len` and all queue caps - remain below their limits. -3. Decrement `send_credit` exactly by `chunk_len` when the frame is - enqueued to ttRPC. -4. If credit is exhausted, pause the upstream reader instead of buffering: - - guest stdout/stderr/PTY readers stop reading from the child fd; - - host stdin readers stop reading from the terminal or pipe; - - detached log replay pauses reading from the retained log source. -5. Resume the reader when new `CreditGrant` bytes arrive. - -This makes the application window, not ttRPC internals, the memory owner. -A process that writes faster than the peer consumes eventually blocks in -its PTY/pipe, or receives typed cancellation if the stall exceeds policy. - -### Receiving DATA and granting credit - -Credit is returned only after bytes are accepted by the downstream sink: - -- CLI stdout/stderr: after the async write to the host fd completes; -- guest stdin: after the write to the child stdin pipe or PTY completes; -- detached logs: after the bounded log sink records the bytes or rejects - them with a typed error. - -The receiver increments `delivered_offset` and accumulates -`grantable_bytes`. It sends `CreditGrant` when either: - -- `grantable_bytes >= window / 2`; or -- `grantable_bytes > 0` and the 10 ms update timer fires; or -- a channel is being closed and the final delivered offset must be - acknowledged. - -A grant carries the cumulative `consumed_offset` and the highest peer -`sequence` observed. The sender treats duplicate or regressive grants as -protocol errors. Grants may be coalesced, but they must never grant bytes -that have not reached the downstream sink. - -### Fairness - -Within one session, control frames have priority over data frames. Data -channels are scheduled round-robin with a per-turn cap of one -`max_frame_bytes` chunk so stdout cannot starve stderr or TTY input. -Across sessions on the same ttRPC connection, the daemon uses weighted -round-robin: - -1. one control frame per ready session; -2. one data chunk per ready session; -3. repeat while the transport accepts work. - -Unary health and lifecycle RPCs run on their normal ttRPC paths and must -not wait behind stream data queues. The conformance gate must run health -traffic while slow-output and blocked-stdin streams are active. - -### Slow-consumer cancellation - -A channel becomes slow when all of the following are true for longer than -the configured grace period: - -- the sender has data ready or an upstream fd is readable; -- `send_credit == 0` or local DATA queues are at their cap; -- the peer has not advanced `consumed_offset` for the channel. - -The sender then sends an `ErrorFrame` with -`ERROR_CODE_SLOW_CONSUMER`, followed by `CloseFrame` with -`CLOSE_REASON_SLOW_CONSUMER` for the affected data channel. For attached -execs, stdout/stderr/TTY slow-consumer cancellation terminates the exec -because the command can no longer be represented faithfully to the user. -For stdin slow-consumer cancellation, the host closes stdin and lets the -process continue unless the CLI requested whole-session cancellation. - -If control frames cannot be delivered because the stream itself is -blocked or disconnected, both sides clean up from local timeout state: -the host reports a typed stream error and the guest terminates or detaches -the exec according to the `ExecCreate` policy. - -### Close and EOF sequencing - -- Host stdin EOF is `CloseFrame{channel: STDIN, reason: EOF, - final_offset}` after all prior stdin DATA frames are sent. -- The guest acknowledges by granting through `final_offset` and then - closing the child stdin fd or sending PTY EOF as appropriate. -- In non-TTY mode the guest separately closes stdout and stderr with - final offsets after the process closes those fds. -- In TTY mode the guest closes the TTY channel once the PTY reaches EOF. -- `ExitFrame` is sent after all process-output DATA and output close - frames have been enqueued. The receiver may display the exit status - only after it has delivered all bytes through the advertised final - offsets. -- `ErrorFrame` may precede `CloseFrame` when a protocol or runtime error - aborts a channel. After a session-level fatal error, no further DATA is - valid. - -A peer rejects DATA after a channel close, close offsets that do not match -the observed byte count, unsupported channels for the negotiated TTY mode, -and exit before output final offsets are known. - -## CLI behavior - -`d2b exec -- ` creates an attached, non-TTY exec with -stdin closed unless `--interactive` is set. The CLI grants stdout and -stderr credit only as writes to its local fds complete. If the user pipes -output to a slow command, guest output naturally blocks at the credit -window and never grows unbounded in daemon memory. - -`--tty` switches output to `TERMINAL_CHANNEL_TTY`, puts the local terminal -in raw mode, sends the initial terminal geometry in `AttachHello`, and -sends `ResizeFrame` on local resize. Raw mode is restored on normal exit, -stream error, cancellation, and panic-safe cleanup paths. - -`--interactive` grants stdin credit from the guest. The CLI reads local -stdin only while the guest has credit. Ctrl-C in TTY mode is transmitted as -terminal input to the foreground process group; non-TTY signal forwarding -uses `SignalFrame` and `ExecSignal` according to the CLI command mode. - -`--detach` creates the exec without an attached stream. Later -`exec attach` uses the same credit-window protocol. `exec logs` may use a -server-streaming log RPC or `ExecAttach` in replay mode, but retained log -bytes are still released only according to client credit. - -Human output mirrors Docker-style behavior: command bytes go only to the -requested stdout/stderr/TTY destinations; protocol errors print concise -redacted diagnostics. The fallback preserves the selected product rule: -attached `exec`/`attach` reject `--json`, while detached run metadata, -`inspect`, and `logs` may use JSON. JSON mode reports bounded envelope -fields such as `exec_id`, `vm`, `outcome`, `exit_code`, `signal`, -`bytes_stdout`, `bytes_stderr`, and `error_code`; it never embeds stdout, -stderr, environment, command arguments, HMAC material, or raw session -tokens except where `logs --json` explicitly returns requested log payloads. - -## Conformance matrix - -| Requirement | Protocol mechanism | Required evidence | -| --- | --- | --- | -| stdin open/EOF | `STDIN` DATA plus `CloseFrame EOF` final offset | Byte-exact stdin and correct child EOF behavior. | -| stdout/stderr split | Separate `STDOUT` and `STDERR` channels | 64 MiB each, byte-exact demux. | -| TTY merge | `TTY` channel only; stdout/stderr invalid | PTY transcript matches expected merged stream. | -| resize ordering | sender `sequence` plus `ResizeFrame` | Reordered resize is rejected or visibly fails test. | -| signal delivery | `SignalFrame` and unary `ExecSignal` | Ctrl-C/SIGTERM reach foreground process group. | -| exit propagation | `ExitFrame` after output close | CLI exit code/signal matches child. | -| bounded memory | channel/session queue caps and reader pausing | Slow consumer stays under budget for 30 s. | -| backpressure | credit returned only after downstream write ack | Writers block or receive slow-consumer cancellation. | -| concurrent sessions | weighted round-robin and bounded queues | No starvation with slow-output, blocked-stdin, TTY, health. | -| cancellation | `ErrorFrame` plus `CloseFrame` and local cleanup | Host disconnect and user cancel reap resources. | -| half-close | per-channel close state machine | stdin close does not imply stdout/stderr close. | -| stale sessions | `generation` in `ExecSessionId` | Old session rejected after guestd restart/reboot. | -| raw-mode cleanup | CLI cleanup around stream lifecycle | Terminal restored on success, error, and cancel. | -| max frame size | ttRPC receive cap before protobuf decode plus bounded post-decode frame validation | max passes; receive-cap + 1 fails before handler entry; effective max + 1 fails with typed error and no session-buffer copy. | -| malformed messages | sequence/channel/window validation | Redacted `ERROR_CODE_PROTOCOL`; no panic. | -| log hygiene | bounded aggregate telemetry only | No command/output/env/token leaks in logs or metrics. | - -## Required tests - -Implementation must add protocol-level tests before production enablement: - -1. fake-transport byte-exact stdout/stderr/stdin tests with payload sizes - at least as large as the feasibility dossier; -2. 30 s slow stdout, stderr, TTY, stdin, and log-replay consumer tests - that assert memory high-water marks and typed cancellation behavior; -3. downstream-write-ack tests using a writer that deliberately stalls - before completing writes, proving credit is not granted early; -4. sequence, offset, duplicate grant, regressive grant, DATA-after-close, - close-offset-mismatch, unsupported-channel, and max+1 frame tests; -5. four-concurrent-session fairness test with a health RPC loop; -6. resize and signal ordering tests against a real PTY; -7. guestd restart, VM reboot, attach-to-detached, host-disconnect, and - daemon-restart cleanup tests; -8. CLI raw-mode restoration tests for success, process signal, protocol - error, local cancellation, and remote disconnect; -9. logging/metrics snapshot tests proving only bounded aggregate fields - are emitted; -10. generated protobuf/ttRPC code checks that preserve the repository's - first-party unsafe policy. - -## Risks and open decisions - -- ttRPC implementations may still buffer or decode whole protobuf messages - before the application sees them. Keeping `max_frame_bytes` small is - mandatory, but the enforceable invariant is a receive cap before - protobuf decode plus bounded post-decode allocation under explicit - per-connection byte budgets. -- Credit logic is more complex than chunked request/response stdio. The - benefit is one full-duplex attach stream with explicit half-close and - interactive latency; the cost is a larger state machine. -- PTY EOF and Ctrl-D behavior differs by program and terminal mode. Tests - must cover shell-like PTY sessions and simple pipe-backed commands. -- Detached log retention needs a bounded sink policy. This protocol bounds - replay to the client, but it does not by itself define retention size or - persistence format. -- Multi-attach semantics are intentionally not selected here. The initial - implementation should allow one interactive attach and reject competing - writers with a typed error; read-only log replay can be added later. -- Window constants may need tuning after real vsock measurements. Changing - defaults is compatible if peers continue to negotiate them in - `AttachHello`. - -## Implementation complexity - -Expected complexity is medium-high. The production change needs a shared -protocol crate, generated protobuf/ttRPC bindings, a tested per-session -state machine, fd reader/writer tasks that can pause and resume cleanly, -PTY integration, CLI terminal cleanup, and conformance tests. It does not -require a custom binary transport, a host proxy daemon, or per-VM systemd -units. - -A safe incremental plan is: - -1. implement the state machine against an in-memory fake transport; -2. add PTY-backed tests for resize, signal, EOF, and exit ordering; -3. connect the state machine to ttRPC async streams over the existing - Cloud Hypervisor vsock transport; -4. wire CLI attach/run/logs behavior; -5. enable the feature only after the full conformance matrix passes. diff --git a/docs/reference/guest-control-persistent-shell.md b/docs/reference/guest-control-persistent-shell.md deleted file mode 100644 index 77d350e11d..0000000000 --- a/docs/reference/guest-control-persistent-shell.md +++ /dev/null @@ -1,114 +0,0 @@ -# Guest-control persistent shell protocol - -> Diataxis: reference. Guest-control shell RPC and terminal transport contract. - -Persistent shell RPCs live on the authenticated guest-control ttRPC service. -They are available only when guestd advertises the shell capabilities for the -running generation. - -## RPCs - -| RPC | Purpose | -| --- | --- | -| `ShellAttach` | Create or attach to a named persistent shell session. | -| `ShellList` | Return the configured default name and known sessions. | -| `ShellDetach` | Detach a named session's live client without killing the shell. | -| `ShellKill` | Terminate a named shell session. | -| `ShellCloseAttach` | Close the current owner attachment. | -| `TerminalWriteStdin` | Write terminal bytes to an attached shell session. | -| `TerminalReadOutput` | Read merged PTY output for an attached shell session. | -| `TerminalCloseStdin` | Close stdin for a terminal session. | -| `TerminalTtyWinResize` | Forward a terminal resize. | - -Shell terminal I/O uses the terminal-generic metadata: - -```text -TerminalRequestMetadata { - common, - session_id, - guest_boot_id, - kind = SHELL -} -``` - -The existing exec-named RPCs remain wire-compatible; persistent shells use the -terminal-generic methods to avoid duplicating the streaming vocabulary. - -## Host Resource and stream mirror - -The host daemon exposes persistent shells as qualified -`shell-terminal.d2bus.org.ShellSession` resources: - -- `Create` opens a named session for a `Host/` or `Guest/` - execution reference; -- `Attach` opens the existing session's authenticated named stream; -- `List`, `Status`, `Detach`, and `Kill` are Resource requests; -- stdin, output, resize, cancellation, and close are named-stream messages. - -The retired `PublicRequest::Shell`, `ShellOp`, and `ShellOpResponse` socket -family is rejected. Provider policy, uid, argv, environment, cwd, and host -paths never enter the Resource request. - -## Local discovery contract - -Local desktop clients such as `d2b-wlterm` do not need to scrape human CLI -output. They list ShellSession resources for an explicit Host or Guest -execution reference and use the typed status projection for session state. - -The current stable list payload is intentionally minimal: - -- `defaultName`; -- `sessions[].name`; -- `sessions[].state`; -- `sessions[].attached`; -- `sessions[].isDefault`. - -That is sufficient for local VM/session discovery and for rendering an -open/stop control surface. The contract does not currently expose attached -owner identity, session handles, last activity timestamps, terminal titles, or -cwd. Those fields are reserved until they can be proven non-leaky and useful; -clients should treat their absence as intentional rather than falling back to -logs, metrics, audit records, or terminal output scraping. - -If ShellSession list or attach returns an error, clients surface the closed -Resource error class such as `unsupported-capability`, -`resource-provider-unavailable`, `resource-not-found`, `backpressure`, or -`timeout`. Clients do not retry into a different provider or transport. - -## States and close causes - -Shell session states are closed enum values: - -- `attached`; -- `detached`; -- `killed`; -- `pool-unavailable`; -- `feature-disabled`; -- `output-gap`. - -Close causes are: - -- `client-detach`; -- `evicted-by-force`; -- `evicted-by-admin-detach`; -- `killed-by-admin`; -- `pool-unavailable`; -- `output-gap`. - -Stale guest boot, guestd instance, or shell-pool epoch mismatches fail closed -rather than silently binding a client to an unrelated session. - -## Stream behavior - -Persistent shells are PTY-backed. Output is merged stdout-style terminal output; -stderr reads are unsupported. Ctrl-C, Ctrl-D, Ctrl-\, and other terminal control -bytes are sent in-band. Resize is the out-of-band terminal control operation. - -## Redaction contract - -DTO `Debug` implementations redact shell names where broad debug output does not -need them, and always redact terminal session handles and terminal byte payloads. -Metrics labels and daemon audit fields never contain raw terminal bytes, helper -stderr/stdout, argv, env, paths, or raw terminal handles. The daemon audit stream -uses a fixed shell correlation digest where it needs to relate attach, detach, or -kill actions. diff --git a/docs/reference/inet-d2b-chains.md b/docs/reference/inet-d2b-chains.md index 27fe7d1e73..d162d55e68 100644 --- a/docs/reference/inet-d2b-chains.md +++ b/docs/reference/inet-d2b-chains.md @@ -122,9 +122,9 @@ kebab-case discriminant and the typed inner detail. (`build_inet_d2b_chains`) - Detector: `d2b_host::nftables::detect_firewall_manager` - Matrix: `d2b_host::nftables::evaluate_coexistence_policy` -- Broker op: `packages/d2b-priv-broker/src/ops/nft.rs` +- Broker op: `packages/d2b-broker/src/ops/nft.rs` (`apply_nftables`) -- USBIP carve-out: `packages/d2b-priv-broker/src/ops/usbip_firewall.rs` +- USBIP carve-out: `packages/d2b-broker/src/ops/usbip_firewall.rs` (`bind_firewall_rule`) - Enforcing coverage: `d2b-host::nftables::tests` and - `d2b-priv-broker::ops::{nft,usbip_firewall}::tests` in `test-rust` + `d2b-broker::ops::{nft,usbip_firewall}::tests` in `test-rust` diff --git a/docs/reference/kernel-module-check.md b/docs/reference/kernel-module-check.md index 2ade847cd7..b8f2a28513 100644 --- a/docs/reference/kernel-module-check.md +++ b/docs/reference/kernel-module-check.md @@ -30,7 +30,7 @@ state lock is acquired and after the pidfd table is restored, but The check does **not** invoke `modprobe`, `modinfo`, or any side-effecting helper. Mutating module load remains the broker's -responsibility (`d2b-priv-broker::ops::modprobe`). +responsibility (`d2b-broker::ops::modprobe`). If `/proc/modules` cannot be read, the check treats *every* module as absent - required modules then read as missing and the daemon diff --git a/docs/reference/manifest-bundle.md b/docs/reference/manifest-bundle.md index 7ebc049d3c..15a3e428df 100644 --- a/docs/reference/manifest-bundle.md +++ b/docs/reference/manifest-bundle.md @@ -111,4 +111,3 @@ references below. | `manifest_v04.json` | [`schemas/v2/manifest_v04.md`](./schemas/v2/manifest_v04.md) | `schemas/v2/manifest_v04.json` | | `wire-protocol.json` | [`schemas/v2/wire-protocol.md`](./schemas/v2/wire-protocol.md) | `schemas/v2/wire-protocol.json` | | unsafe-local helper protocol | [`schemas/v2/unsafe-local-helper-wire.md`](./schemas/v2/unsafe-local-helper-wire.md) | `schemas/v2/unsafe-local-helper-wire.json` | -| guest-control protocol | [`schemas/v2/guest-control.md`](./schemas/v2/guest-control.md) | `schemas/v2/guest-control.json` | diff --git a/docs/reference/manifest-schema.json b/docs/reference/manifest-schema.json index f4456ded96..07865319af 100644 --- a/docs/reference/manifest-schema.json +++ b/docs/reference/manifest-schema.json @@ -4,12 +4,17 @@ "title": "d2b JSON manifest", "description": "Per-VM metadata manifest emitted by the d2b NixOS framework at /run/current-system/sw/share/d2b/vms.json. The contract consumed by the Rust CLI and daemon. See docs/reference/manifest-schema.md for the prose walkthrough and compatibility policy.", "type": "object", - "required": ["_manifest", "_observability"], + "required": [ + "_manifest", + "_observability" + ], "properties": { "_manifest": { "type": "object", "description": "Reserved schema-metadata key. The leading underscore disqualifies it as a VM name (vmNameOk regex requires a leading lowercase letter), so it cannot collide with any valid consumer config.", - "required": ["manifestVersion"], + "required": [ + "manifestVersion" + ], "additionalProperties": false, "properties": { "manifestVersion": { @@ -33,7 +38,15 @@ "observabilityMeta": { "type": "object", "description": "Reserved observability-metadata key. Always emitted so consumers can discover the observability stack wiring without looking inside any single VM entry.", - "required": ["enabled", "vmName", "obsVsockCid", "obsVsockHostSocket", "signozUrl", "signozOtlpGrpcPort", "signozOtlpHttpPort"], + "required": [ + "enabled", + "vmName", + "obsVsockCid", + "obsVsockHostSocket", + "signozUrl", + "signozOtlpGrpcPort", + "signozOtlpHttpPort" + ], "additionalProperties": false, "properties": { "enabled": { @@ -75,7 +88,12 @@ "vmObservability": { "type": "object", "description": "Per-VM observability transport metadata. Always emitted so consumers can rely on a stable transport metadata location.", - "required": ["enabled", "vsockCid", "vsockHostSocket", "agentSocket"], + "required": [ + "enabled", + "vsockCid", + "vsockHostSocket", + "agentSocket" + ], "additionalProperties": false, "properties": { "enabled": { @@ -83,17 +101,26 @@ "description": "True iff d2b.vms..observability.enable is set." }, "vsockCid": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 0, "maximum": 4294967295, - "description": "Deterministic base Cloud Hypervisor vsock CID for nixos/Cloud Hypervisor VMs. Null for providers without d2b guest-control or in-guest observability." + "description": "Deterministic base Cloud Hypervisor vsock CID for nixos/Cloud Hypervisor VMs. Null for providers without d2b ComponentSession or in-guest observability." }, "vsockHostSocket": { - "type": ["string", "null"], - "description": "Host-side Unix socket backing this VM's Cloud Hypervisor vsock device. Null for providers without d2b guest-control or in-guest observability." + "type": [ + "string", + "null" + ], + "description": "Host-side Unix socket backing this VM's Cloud Hypervisor vsock device. Null for providers without d2b ComponentSession or in-guest observability." }, "agentSocket": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "In-guest Unix socket reserved for the workload observability agent. Null for providers without in-guest observability." } } @@ -101,7 +128,10 @@ "vmLanPolicy": { "type": "object", "description": "Per-VM LAN east-west policy metadata for env-backed VMs.", - "required": ["allowEastWest", "effectiveEastWest"], + "required": [ + "allowEastWest", + "effectiveEastWest" + ], "additionalProperties": false, "properties": { "allowEastWest": { @@ -117,12 +147,19 @@ "runtimeProvider": { "type": "object", "description": "Local runtime provider identity.", - "required": ["id", "type", "driver"], + "required": [ + "id", + "type", + "driver" + ], "additionalProperties": false, "properties": { "id": { "type": "string", - "enum": ["local-cloud-hypervisor", "local-qemu-media"], + "enum": [ + "local-cloud-hypervisor", + "local-qemu-media" + ], "description": "Stable runtime provider identifier." }, "type": { @@ -132,7 +169,10 @@ }, "driver": { "type": "string", - "enum": ["cloud-hypervisor", "qemu"], + "enum": [ + "cloud-hypervisor", + "qemu" + ], "description": "Provider driver family." } } @@ -140,82 +180,182 @@ "runtimeCapabilities": { "type": "object", "description": "Runtime/provider support matrix. These booleans describe support, not whether a per-VM option currently enables a feature.", - "required": ["lifecycle", "display", "usbHotplug", "guestControl", "exec", "configSync", "ssh", "storeSync", "keys", "inGuestObservability"], + "required": [ + "lifecycle", + "display", + "usbHotplug", + "exec", + "configSync", + "ssh", + "storeSync", + "keys", + "inGuestObservability" + ], "additionalProperties": false, "properties": { - "lifecycle": { "type": "boolean" }, - "display": { "type": "boolean" }, - "usbHotplug": { "type": "boolean" }, - "guestControl": { "type": "boolean" }, - "exec": { "type": "boolean" }, - "configSync": { "type": "boolean" }, - "ssh": { "type": "boolean" }, - "storeSync": { "type": "boolean" }, - "keys": { "type": "boolean" }, - "inGuestObservability": { "type": "boolean" } + "lifecycle": { + "type": "boolean" + }, + "display": { + "type": "boolean" + }, + "usbHotplug": { + "type": "boolean" + }, + "exec": { + "type": "boolean" + }, + "configSync": { + "type": "boolean" + }, + "ssh": { + "type": "boolean" + }, + "storeSync": { + "type": "boolean" + }, + "keys": { + "type": "boolean" + }, + "inGuestObservability": { + "type": "boolean" + } } }, "runtimeOperationCapabilities": { "type": "object", "description": "Positive runtime operation support grouped by public feature axis.", - "required": ["lifecycle", "media", "display", "guest", "storage"], + "required": [ + "lifecycle", + "media", + "display", + "guest", + "storage" + ], "additionalProperties": false, "properties": { "lifecycle": { "type": "object", - "required": ["start", "stop", "restart", "switch", "hostPrepare"], + "required": [ + "start", + "stop", + "restart", + "switch", + "hostPrepare" + ], "additionalProperties": false, "properties": { - "start": { "type": "boolean" }, - "stop": { "type": "boolean" }, - "restart": { "type": "boolean" }, - "switch": { "type": "boolean" }, - "hostPrepare": { "type": "boolean" } + "start": { + "type": "boolean" + }, + "stop": { + "type": "boolean" + }, + "restart": { + "type": "boolean" + }, + "switch": { + "type": "boolean" + }, + "hostPrepare": { + "type": "boolean" + } } }, "media": { "type": "object", - "required": ["usbHotplug", "removableMedia", "qemuMedia"], + "required": [ + "usbHotplug", + "removableMedia", + "qemuMedia" + ], "additionalProperties": false, "properties": { - "usbHotplug": { "type": "boolean" }, - "removableMedia": { "type": "boolean" }, - "qemuMedia": { "type": "boolean" } + "usbHotplug": { + "type": "boolean" + }, + "removableMedia": { + "type": "boolean" + }, + "qemuMedia": { + "type": "boolean" + } } }, "display": { "type": "object", - "required": ["display", "graphics", "video", "waylandProxy"], + "required": [ + "display", + "graphics", + "video", + "waylandProxy" + ], "additionalProperties": false, "properties": { - "display": { "type": "boolean" }, - "graphics": { "type": "boolean" }, - "video": { "type": "boolean" }, - "waylandProxy": { "type": "boolean" } + "display": { + "type": "boolean" + }, + "graphics": { + "type": "boolean" + }, + "video": { + "type": "boolean" + }, + "waylandProxy": { + "type": "boolean" + } } }, "guest": { "type": "object", - "required": ["guestControl", "exec", "shell", "configSync", "ssh", "keys", "inGuestObservability"], + "required": [ + "exec", + "shell", + "configSync", + "ssh", + "keys", + "inGuestObservability" + ], "additionalProperties": false, "properties": { - "guestControl": { "type": "boolean" }, - "exec": { "type": "boolean" }, - "shell": { "type": "boolean" }, - "configSync": { "type": "boolean" }, - "ssh": { "type": "boolean" }, - "keys": { "type": "boolean" }, - "inGuestObservability": { "type": "boolean" } + "exec": { + "type": "boolean" + }, + "shell": { + "type": "boolean" + }, + "configSync": { + "type": "boolean" + }, + "ssh": { + "type": "boolean" + }, + "keys": { + "type": "boolean" + }, + "inGuestObservability": { + "type": "boolean" + } } }, "storage": { "type": "object", - "required": ["storeSync", "virtiofs", "volumes"], + "required": [ + "storeSync", + "virtiofs", + "volumes" + ], "additionalProperties": false, "properties": { - "storeSync": { "type": "boolean" }, - "virtiofs": { "type": "boolean" }, - "volumes": { "type": "boolean" } + "storeSync": { + "type": "boolean" + }, + "virtiofs": { + "type": "boolean" + }, + "volumes": { + "type": "boolean" + } } } } @@ -223,7 +363,11 @@ "runtimeServiceSummary": { "type": "object", "description": "Provider-neutral runtime service summary.", - "required": ["id", "role", "optional"], + "required": [ + "id", + "role", + "optional" + ], "additionalProperties": false, "properties": { "id": { @@ -232,7 +376,19 @@ }, "role": { "type": "string", - "enum": ["host", "hypervisor", "storage", "tpm", "display", "audio", "video", "network", "guest-control", "usb", "observability"], + "enum": [ + "host", + "hypervisor", + "storage", + "tpm", + "display", + "audio", + "video", + "network", + "usb", + "observability", + "component-session" + ], "description": "Low-cardinality service role." }, "optional": { @@ -243,8 +399,13 @@ }, "vmShell": { "type": "object", - "description": "Persistent guest shell policy metadata for providers that support the authenticated guest-control terminal substrate. Null for runtime providers without d2b guest-control.", - "required": ["enabled", "defaultName", "maxSessions", "maxAttached"], + "description": "Persistent guest shell policy metadata for providers that support the authenticated ComponentSession terminal substrate. Null for runtime providers without d2b ComponentSession.", + "required": [ + "enabled", + "defaultName", + "maxSessions", + "maxAttached" + ], "additionalProperties": false, "properties": { "enabled": { @@ -273,7 +434,10 @@ "vmGracefulShutdown": { "type": "object", "description": "Per-VM graceful guest shutdown policy consumed by d2bd.", - "required": ["enable", "timeoutSeconds"], + "required": [ + "enable", + "timeoutSeconds" + ], "additionalProperties": false, "properties": { "enable": { @@ -281,7 +445,10 @@ "description": "True iff d2bd should attempt provider-aware graceful guest shutdown before falling back to forced VMM cleanup for this VM." }, "timeoutSeconds": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "minimum": 1, "maximum": 600, "description": "Optional per-VM graceful shutdown timeout override, in seconds. Null means daemon-config.json gracefulShutdownTimeoutSeconds applies." @@ -291,7 +458,9 @@ "vmLifecycle": { "type": "object", "description": "Per-VM lifecycle policy consumed by d2bd.", - "required": ["gracefulShutdown"], + "required": [ + "gracefulShutdown" + ], "additionalProperties": false, "properties": { "gracefulShutdown": { @@ -302,12 +471,22 @@ "runtimeMetadata": { "type": "object", "description": "Runtime/provider metadata and capability summary for one VM.", - "required": ["kind", "provider", "capabilities", "operationCapabilities", "autostartPolicy", "services"], + "required": [ + "kind", + "provider", + "capabilities", + "operationCapabilities", + "autostartPolicy", + "services" + ], "additionalProperties": false, "properties": { "kind": { "type": "string", - "enum": ["nixos", "qemu-media"], + "enum": [ + "nixos", + "qemu-media" + ], "description": "VM runtime family." }, "provider": { @@ -321,7 +500,12 @@ }, "autostartPolicy": { "type": "string", - "enum": ["unknown", "host-boot-eligible", "manual-only", "disabled"], + "enum": [ + "unknown", + "host-boot-eligible", + "manual-only", + "disabled" + ], "description": "Runtime-level autostart policy." }, "services": { @@ -405,11 +589,17 @@ "description": "Host-side tap-device name. Derived: -l (workload), -u2 (net VM), or vm- (legacy)." }, "bridge": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Linux bridge the tap attaches to. Workload: br--lan. Net VM: br--up. Legacy hand-rolled VM: null." }, "env": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Env this VM belongs to (workload) or serves (net VM). Null for legacy hand-rolled VMs." }, "mtu": { @@ -432,11 +622,17 @@ "description": "True iff this VM is the auto-generated sys--net for some env." }, "netVm": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "For workload VMs: name of the net VM serving this VM's env. Null for net VMs themselves and legacy VMs." }, "usbipdHostIp": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Host IP of the per-env usbipd proxy. `d2b usb attach` passes this to `usbip attach -r` via the broker SpawnRunner DAG. The legacy `d2b usb ` bash orchestrator was retired per ADR 0015. Null for net VMs and legacy." }, "stateDir": { @@ -444,23 +640,38 @@ "description": "Per-VM state dir. Currently /var/lib/d2b/vms/." }, "apiSocket": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Cloud Hypervisor runner API socket path (/.sock). Null for providers without a Cloud Hypervisor API socket." }, "gpuSocket": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "crosvm-gpu sidecar control socket (/-gpu.sock). Null for providers without a d2b GPU sidecar socket." }, "tpmSocket": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "swtpm vTPM socket path (/run/d2b/vms//tpm.sock). Null for providers without d2b-managed TPM state." }, "audioStateFile": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Live audio-grant state file (/state/audio-state.json). Null for providers without the d2b audio sidecar." }, "audioService": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Retired field. Always null; the audio sidecar is broker-spawned through the daemon DAG." }, "observability": { @@ -468,17 +679,27 @@ }, "shell": { "anyOf": [ - { "$ref": "#/$defs/vmShell" }, - { "type": "null" } + { + "$ref": "#/$defs/vmShell" + }, + { + "type": "null" + } ], - "description": "Persistent guest shell policy metadata, or null for providers without d2b guest-control." + "description": "Persistent guest shell policy metadata, or null for providers without d2b ComponentSession." }, "staticIp": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "VM's static LAN IP. Derived for env-attached VMs. Null when no IP source applies." }, "sshUser": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "Username for d2b-driven SSH. Mirrors d2b.vms..ssh.user. Null for headless net VMs." } } diff --git a/docs/reference/manifest-schema.md b/docs/reference/manifest-schema.md index 16bd02b5b2..3327fed4eb 100644 --- a/docs/reference/manifest-schema.md +++ b/docs/reference/manifest-schema.md @@ -14,7 +14,7 @@ ships it as: /run/current-system/sw/share/d2b/vms.json ``` -The Rust CLI, `d2bd`, and `d2b-priv-broker` consume this public +The Rust CLI, `d2bd`, and `d2b-broker` consume this public inventory. Private bundle artifacts live beside it and are documented in [`manifest-bundle.md`](./manifest-bundle.md). @@ -46,7 +46,6 @@ inventory. Private bundle artifacts live beside it and are documented in "lifecycle": true, "display": true, "usbHotplug": true, - "guestControl": true, "exec": true, "configSync": true, "ssh": true, @@ -74,7 +73,6 @@ inventory. Private bundle artifacts live beside it and are documented in "waylandProxy": true }, "guest": { - "guestControl": true, "exec": true, "shell": true, "configSync": true, @@ -152,7 +150,7 @@ Fields are listed in `nixos-modules/manifest.nix` declaration order. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | VM name; matches the enclosing top-level key. Pattern `^[a-z][a-z0-9-]*$` (enforced by `nixos-modules/assertions.nix`). | -| `runtime` | object | yes | Runtime/provider metadata and provider support matrix. Shape: `{ kind, provider: { id, type, driver }, capabilities, operationCapabilities, autostartPolicy, services }`. `operationCapabilities` groups positive operation support by lifecycle/media/display/guest/storage axis; `operationCapabilities.guest.shell` records provider support for the staged persistent-shell operation. `services[]` contains bounded provider-neutral service summaries. `qemu-media` uses provider `local-qemu-media`/driver `qemu`; its supported capabilities are lifecycle/display/USB hotplug, while guest-control, exec, shell, config-sync, SSH, store-sync, keys, and in-guest observability are unsupported. | +| `runtime` | object | yes | Runtime/provider metadata and provider support matrix. Shape: `{ kind, provider: { id, type, driver }, capabilities, operationCapabilities, autostartPolicy, services }`. `operationCapabilities` groups positive operation support by lifecycle/media/display/guest/storage axis; `operationCapabilities.guest.shell` records provider support for the staged persistent-shell operation. `services[]` contains bounded provider-neutral service summaries. `qemu-media` uses provider `local-qemu-media`/driver `qemu`; its supported capabilities are lifecycle/display/USB hotplug, while exec, shell, config-sync, SSH, store-sync, keys, and in-guest observability are unsupported. | | `lifecycle` | object | yes | Per-VM lifecycle policy. Shape: `{ gracefulShutdown: { enable, timeoutSeconds }, liveActivation: { timeoutSeconds } }`. `gracefulShutdown` controls provider-aware guest shutdown before forced VMM cleanup; its timeout is a nullable 1-600 second per-VM override. `liveActivation.timeoutSeconds` is a nullable 1-3600 second per-VM override for in-guest `switch`/`test`/`rollback`; `null` means the daemon default from `/etc/d2b/daemon-config.json` applies. | | `autostart` | boolean | yes | Mirror of `d2b.vms..autostart`. When true, the VM is eligible for d2bd's host-boot autostart pass; graphics and qemu-media provider guards can still make a VM manual-only. Older v6/v7 manifests may omit this additive field and are treated as opting in for compatibility with the former heuristic. | | `graphics` | boolean | yes | Mirror of `d2b.vms..graphics.enable`. The CLI uses it to pick the launch path. | @@ -176,7 +174,7 @@ Fields are listed in `nixos-modules/manifest.nix` declaration order. | `audioStateFile` | string \| null | yes | Live audio-grant state file (`/state/audio-state.json`). Null for providers without the d2b audio sidecar. | | `audioService` | string \| null | yes | Retired field. Always null; the audio sidecar is broker-spawned through the daemon DAG. | | `observability` | object | yes | Per-VM observability transport metadata (`enabled`, base `vsockCid`/`vsockHostSocket`, guest `agentSocket`). See [Per-VM observability block](#per-vm-observability-block). | -| `shell` | object \| null | yes | Persistent guest shell policy metadata for providers that support the authenticated guest-control terminal substrate. Null for providers without d2b guest-control. Shape: `{ enabled, defaultName, maxSessions, maxAttached }`; `defaultName` matches `^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$`, `maxSessions` is 1-256, and `maxAttached` is 1-64. This is policy/capability metadata only; runtime helper sockets, shpool state, terminal handles, and session names beyond the configured default are never included in the world-readable manifest. | +| `shell` | object \| null | yes | Persistent guest shell policy metadata for providers that support the authenticated ComponentSession terminal substrate. Null for providers without ComponentSession. Shape: `{ enabled, defaultName, maxSessions, maxAttached }`; `defaultName` matches `^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$`, `maxSessions` is 1-256, and `maxAttached` is 1-64. This is policy/capability metadata only; runtime helper sockets, shpool state, terminal handles, and session names beyond the configured default are never included in the world-readable manifest. | | `staticIp` | string \| null | yes | The VM's static LAN IP. Derived for env-attached VMs; null when no IP source applies. | | `sshUser` | string \| null | yes | Username for `d2b`-driven SSH. Mirrors `d2b.vms..ssh.user`. Null for headless net VMs. | @@ -200,11 +198,11 @@ Version history: Hypervisor vsock semantics - the per-VM `observability.vsockCid` / `observability.vsockHostSocket` fields define the host-owned base Cloud Hypervisor vsock device shared by observability and guest - control, not only the observability relay. These two changes each + session, not only the observability relay. These two changes each landed as a `4` on separate branches and are unified at `5`. - v6: adds per-VM runtime/provider metadata and provider capability summaries. Provider-specific socket/vsock fields are now nullable so - `qemu-media` entries do not fabricate Cloud Hypervisor, guest-control, + `qemu-media` entries do not fabricate Cloud Hypervisor, ComponentSession, SSH, store-sync, key, or in-guest-observability artifacts. - v6 additive: adds per-VM nullable `shell` policy metadata and `runtime.operationCapabilities.guest.shell`. This is additive and does not bump @@ -242,8 +240,8 @@ about the vsock path without knowing SigNoz internals. | Field | Type | Required | Description | | --- | --- | --- | --- | | `enabled` | boolean | yes | Whether telemetry collection is enabled for this VM. | -| `vsockCid` | unsigned integer \| null | yes | Deterministic base Cloud Hypervisor vsock CID for nixos/Cloud Hypervisor VMs. Null for providers without d2b guest-control or in-guest observability. | -| `vsockHostSocket` | string \| null | yes | Host-side Cloud Hypervisor vsock socket for this VM. Null for providers without d2b guest-control or in-guest observability. | +| `vsockCid` | unsigned integer \| null | yes | Deterministic base Cloud Hypervisor vsock CID for nixos/Cloud Hypervisor VMs. Null for providers without d2b ComponentSession or in-guest observability. | +| `vsockHostSocket` | string \| null | yes | Host-side Cloud Hypervisor vsock socket for this VM. Null for providers without d2b ComponentSession or in-guest observability. | | `agentSocket` | string \| null | yes | Guest-local OTLP socket path used by the guest collector. Null for providers without in-guest observability. | The per-VM block is emitted for every VM so clients do not need to infer diff --git a/docs/reference/naming-conventions.md b/docs/reference/naming-conventions.md index a02ac78aa7..5ab024a1fd 100644 --- a/docs/reference/naming-conventions.md +++ b/docs/reference/naming-conventions.md @@ -7,7 +7,7 @@ Canonical reference for host-visible d2b naming. AGENTS.md and the design doc ke | Resource | Pattern | Notes | | --- | --- | --- | | Daemon | `d2bd.service` | v1.0 non-root daemon; the only persistent user-facing system unit d2b declares besides the broker (per ADR 0015). | -| Privileged broker | `d2b-priv-broker.{socket,service}` | v1.0 socket-activated privileged dispatcher for every audited host mutation. | +| Privileged broker | `d2b-broker.{socket,service}` | v1.0 socket-activated privileged dispatcher for every audited host mutation. | | Per-VM runner leaves | `d2b.slice//` | v1.0 broker-spawned runner cgroup leaves (replaces legacy `d2b--{gpu,video,snd,swtpm,store-sync}.service`). | | Per-env runner leaves | `d2b.slice/sys-/usbipd-{backend,proxy}` | v1.0 broker-spawned per-env USBIP runners (replaces legacy `d2b-sys--usbipd-{backend,proxy}.service`). | | Legacy per-VM lifecycle wrapper | `d2b@.service` | Retired in v1.0 (per ADR 0015); v1.0 lifecycle dispatches through `d2bd` → broker `SpawnRunner` instead. | @@ -60,7 +60,7 @@ public workload target form is: During the v2 transition, `d2b.realms..workloads..legacyVmName` maps that public workload id to the existing local VM substrate. For example, `workloads.aad.legacyVmName = "work-aad"` makes `aad.work.d2b` resolve to the -local `work-aad` VM for status and guest-control exec while preserving +local `work-aad` VM for status and ComponentSession exec while preserving `/var/lib/d2b/vms/work-aad`. These constraints let the CLI, manifest, and host-side units resolve resources diff --git a/docs/reference/privileges.md b/docs/reference/privileges.md index 94878a209e..c530fcb70c 100644 --- a/docs/reference/privileges.md +++ b/docs/reference/privileges.md @@ -1,7 +1,7 @@ # Reference: broker privileged operation matrix > Diataxis: reference. Stable catalog of every closed-enum operation -> the privileged broker (`d2b-priv-broker`) is allowed to perform +> the privileged broker (`d2b-broker`) is allowed to perform > on behalf of the unprivileged `d2bd` daemon. The wire-level > source of truth is `d2b_contracts::BrokerRequest`; this page is the > human-readable index keyed by operation name. @@ -17,7 +17,6 @@ Every row carries three policy flags: - **secret** - `yes` for operations whose implementation reads secret material or whose audit record may reference secret-material identifiers. `redacted-only` rows carry only derived/redacted metadata: - for example `GuestControlSign` records token-transcript metadata (`transcript_len`, `peer_cid_present`, `capabilities_hash_present`), and `UsbipBind` records normalized device identity plus serial HMAC correlations, never the per-VM token, signature bytes, raw serial, raw @@ -26,6 +25,17 @@ Every row carries three policy flags: Unknown variants and unknown fields in security-sensitive artifacts are denied (`defaultForUnknown: deny`). +## Mode-bound broker instances + +Host and Guest authorities use the same `d2b-broker` artifact, selected at +process start as `d2b-broker host` or `d2b-broker guest`. The active profile is +not carried on the request wire and cannot be changed by a caller. + +Each authority instance has its own socket, caller UID/GID, state root, audit +root, and authority label. Guest mode exposes only the closed local-process +effect catalog; Host-only networking, device, storage, realm, and allocator operations are +refused before dispatch. + > **Authz-class vs system-group naming note.** The **Allowed groups** > column below uses the broker's **authz class** identifiers > `d2b-launcher` (singular) and `d2b-admin`, which are the @@ -114,13 +124,6 @@ are denied (`defaultForUnknown: deny`). > subject to the Zone Role and RoleBinding policy; the operation row only > classifies the authenticated local daemon request. -> **Host cutover is an admin-only public daemon operation.** The `host cutover` -> command family is destructive, retains metadata-only secret access, and -> requires broker admission. It appears in `PrivilegesJson.publicOperations` -> with only the `d2b-admin` authz class; the daemon enforces that classification -> before any cutover broker request is emitted. See the [CLI contract](cli-contract.md) -> and [U4 cutover runner admission](#u4-cutover-runner-admission). - ## Operation catalog (PROTOCOL_VERSION = 5) The currently implemented broker operation catalog. Every row carries @@ -132,9 +135,6 @@ The currently implemented broker operation catalog. Every row carries | `OpenCgroupDir` | cgroup | per VM / role | live | no | no | `d2b-launcher` + `d2b-admin` | yes | deny | `cgroup_id`, `path_class` | [0011](../adr/0011-cgroup-v2-delegation-and-pidfd-handoff.md) | | `CgroupKill` | cgroup | per VM / role leaf | live | yes (writes `cgroup.kill` on a leaf) | no | `d2b-launcher` + `d2b-admin` | yes | deny | Request DTO (opaque IDs only): `vm_id`, `role_id`. Audit `operation_fields` (broker-derived after subject resolution): `cgroup_id`, `path_class` (one of `vm-role-leaf` / `host-scoped-leaf`; omitted on subject-resolution failure, which is recorded as `decision: denied-unknown` + `error_kind: unknown-subject` in the audit header). Refused with `cgroup-kill-on-ancestor-refused` if the resolved path's `path_class` is `slice` / `vm-interior`. The daemon invokes this op only as last-resort escalation after `pidfd_send_signal(SIGTERM)` does not drain the leaf within the role's grace period. | [0011](../adr/0011-cgroup-v2-delegation-and-pidfd-handoff.md) | | `ApplyHostGenerationHandoff` | host generation | global | live | yes | metadata-only | `d2bd` | yes | deny | Source and target generation identifiers plus fixed outcome metadata; raw store paths and activation arguments never cross the wire. | [ADR-046 activation-nixos](../specs/providers/ADR-046-provider-activation-nixos.md) | -| `LaunchCutoverRunner` | cutover-runner | global | live | yes | metadata-only | `d2bd` | yes | deny | `operation_id`, `capability_digest`, `expires_at_ms`, `bootstrap_fd_index` | U4 cutover runner admission | -| `CutoverAudit` | cutover-audit | global | live | no | metadata-only | `d2b-cutover-runner` | yes | deny | `operation_id`, `phase`, `transition`, `request_digest`, `reason_digest` | U4 cutover runner admission | -| `CutoverEffect` | cutover-effect | global | live | yes | metadata-only | `d2b-cutover-runner` | yes | deny | `operation_id`, `authority`, `phase`, `effect_id`, `effect`, `replay_class`, `request_digest`, `capability_digest`, `outcome` | U4 cutover runner admission | | `ObserveRunner` | runner | per VM / role | live | no | metadata-only | `d2bd` | yes | deny | Bounded process identity, start-time, cgroup, and executable verification metadata. | [0011](../adr/0011-cgroup-v2-delegation-and-pidfd-handoff.md) | | `PipeWireAudio` | audio | per VM / role / channel | live | yes | no | `d2bd` | yes | deny | Bounded channel and action outcome; no PipeWire path, node identifier, or stream handle. | [ADR-046 audio-pipewire](../specs/providers/ADR-046-provider-audio-pipewire.md) | | `StartSystemdUnit` | systemd unit | per VM / role | live | yes | no | `d2bd` | yes | deny | Typed transient-unit identity and bounded start outcome; free-form properties are refused. | [ADR-046 system-systemd](../specs/providers/ADR-046-provider-system-systemd.md) | @@ -209,7 +209,6 @@ broker request is emitted. | `PrepareStoreView` | fs (store view) | per VM | live | yes | no | `d2b-launcher` + `d2b-admin` | yes | deny | `generation`, `hardlink_farm_path`, `target_view_path` | | `StoreSync` | fs (hardlink farm) | per VM | live | yes (atomic `current` symlink swap) | no | `d2b-launcher` + `d2b-admin` | yes | deny | `bundle_closure_ref`, `generation`, `closure_count`, `hardlink_farm_path` | | `OpenZoneStore` | resource store | per Zone | live | yes (provision/open and identity validation) | metadata-only | `d2bd` | yes | deny | `zone_store_id`, `store_identity`, `disposition`, `fd_count` (no host path) | -| `GuestControlSign` | guest-control token | per VM | live | no | redacted-only | `d2bd` | yes | deny | `vm_id`, `role`, `purpose`, `transcript_len`, `peer_cid_present`, `capabilities_hash_present` | | `SetupMountNamespace` | mount ns | per VM / role | live | partial (mount-root prep + bind target) | no | `d2b-launcher` + `d2b-admin` | yes | deny | `role_id`, `mount_root`, `mount_view_path`, `source_view_path` | | `DeregisterRunnerPidfd` | process registry | per VM / role | live | no | no | `d2b-launcher` + `d2b-admin` | yes | deny | `vm_id`, `role_id`, `removed` | | `DiskInit` | disk image | per VM | live | yes (create/format declared images; repair safe declared posture drift) | no | `d2bd` | yes | deny | `vm_id`, `ops_total`, `ops_created`, `ops_skipped`, `ops_repaired`, `ops_posture_repaired`, `target_paths_hash` | @@ -279,33 +278,10 @@ non-bootstrap dispatch surface as typed per-op payloads: - `SignalRunner { vm_id, role_id, signal }` - `DeregisterRunnerPidfd { vm_id, role_id }` - `SpawnRunner { bundle_runner_intent_ref, vm_id, role_id, role, runtime_allocations }` - - when the runner is the cloud-hypervisor role of a guest-control VM, - spawning it also grants the unprivileged `d2bd` daemon uid a - narrow ACL on the per-VM vsock transport (traversal `--x` on the - non-public state-dir components, `rw` on `vsock.sock`) so the daemon - can drive the authenticated guest-control bridge. The grant runs as a - revoke-then-grant at each cloud-hypervisor (re-)spawn: any stale - daemon grant left on a replaced/disabled socket inode is revoked - first, then the traversal chain and the live-socket `rw` grant are - (re-)established, so a disabled or replaced socket cannot retain a - stale daemon grant. (A dedicated stop-time teardown revoke hook is - future work: `SignalRunner` carries no socket path.) This ACL is - a SpawnRunner **side-effect**, not a separate broker op. Both the grant - and the revoke emit a hash-only audit record carrying `target_class` - (`state-dir`, `ancestor`, or `vsock-socket`), the `daemon_principal`, - and `acl_diff_hash` + `result` - never the raw socket / state-dir path. - A **second** SpawnRunner side-effect on the same guest-control - cloud-hypervisor spawn grants the cloud-hypervisor **runner** uid connect - access to the cross-principal `d2b-gctl` token fs-share socket: `--x` - traversal on the per-VM `guest-control` dir and `rw` (with an explicit - `m::rw` mask) on the `gctlfs`-owned `d2b-gctl` socket inode, scoped to the - live `(dev, ino)` (re-fstat after the fd-based setfacl) and retried until - the socket is bound. This lifts the 0700 socket's `mask::---`, which - otherwise masks out the inherited `default:u:` grant and EACCESes - cloud-hypervisor's vhost-user connect. It emits its own hash-only audit - record carrying `target_class` (`gctlfs-dir` or `gctlfs-socket`), the - `consumer_principal` (`cloud-hypervisor-runner`), and `acl_diff_hash` + - `result` - never the raw socket / state-dir path or a uid-by-value. + validates the signed runner intent, target execution binding, and profile before + launching the declared Provider Process. ComponentSession endpoints are + enrolled from trusted bundle metadata; no token share or fallback + transport is part of the operation. - `RunHostInstall { bundle_installer_intent_ref, enable, start, no_start }` - `RunMigrate { bundle_migrate_intent_ref }` - `RunActivation { bundle_activation_intent_ref, mode, vm }` @@ -413,7 +389,7 @@ mountPolicy.bindMounts = [ This section documents broker ops used by the daemon-side host-prep DAG and the daemon-side preflights that run beside them. The current daemon-only surface is `d2bd.service` + -`d2b-priv-broker.{service,socket}` + per-VM runners spawned via +`d2b-broker.{service,socket}` + per-VM runners spawned via broker `SpawnRunner`. `StoreSync` is live. `SshKeygenProbe` remains documented as future @@ -457,7 +433,7 @@ records the former systemd template that no longer exists. | `host-prep.bridge` | `microvm-tap-interfaces@.service` (bridge-port subset); replaced by `SetBridgePortFlags` broker dispatch | `SetBridgePortFlags` | after `host-prep.sysctl`, before `host-prep.spawn` | | `host-prep.pci-devices` | `microvm-pci-devices@.service`; replaced by `OpenDevice` broker dispatch | `OpenDevice` | parallel with `host-prep.tap` chain; joins before `host-prep.spawn` | | `host-prep.store-sync` | `d2b--store-sync.service` + activation-time `d2b-store-sync` call from `store.nix`; replaced by `StoreSync` broker dispatch | `StoreSync` (live) | before any per-VM runner spawn; for the host-install/apply path, runs as part of `host install --apply` | -| `host-prep.known-hosts-refresh` | `d2b-known-hosts-refresh@.service`; current replacement is the planned `SshKeygenProbe` broker dispatch | `SshKeygenProbe` (future work) | after `vm.guest-control-health`, not in the cold-start chain | +| `host-prep.known-hosts-refresh` | `d2b-known-hosts-refresh@.service`; current replacement is the planned `SshKeygenProbe` broker dispatch | `SshKeygenProbe` (future work) | after `vm.component-session-health`, not in the cold-start chain | | `vm.set-booted` | `microvm-set-booted@.service`; replaced by pure-daemon `supervisor::state::record_booted(, )` (no broker op) | - (pure daemon: `supervisor::state::record_booted(, )`) | after runner reports ready; no broker call | | `host-prep.spawn` | - (final join) | `SpawnRunner` | after every preceding `host-prep.*` node completes; carries SCM_RIGHTS handoff of fds from `CreateTapFd` / `OpenDevice` / `OpenKvm` / etc. | @@ -515,8 +491,8 @@ enforces fail-closed before fork/exec. | Runner role | Legacy unit replaced | Caps (steady-state) | Per-env scope | Broker-dispatch contract | | --- | --- | --- | --- | --- | | `OtelHostBridge` | `d2b-otel-host-bridge.service`; replaced by broker `SpawnRunner{role: OtelHostBridge, …}` | empty | host-scoped (singleton - exactly one runner per host) | Broker refuses `SpawnRunner{role: OtelHostBridge, …}` fail-closed via `Broker.OtelHostBridgeIntentInvalid` unless the bundle's `OtelHostBridge` runner intent points at a VM whose `vm_name` equals `manifest._observability.vmName`. Readiness gate: `/run/d2b/otel` exists with expected ownership, stale `host-egress.sock` is removed, and the obs VM base `vsock.sock` exists; exponential backoff applies on host-OTLP unreachable. Broker waits for the readiness gate before exec; `supervisor::pidfd` respawns on relay exit. Pre-opened vsock fds only; `socket(AF_VSOCK)` is denied by `w1-otel-host-bridge` seccomp. | -| `Usbip` | per-env singletons `d2b-sys--usbipd-{proxy,backend}.{service,socket}`; replaced by broker `SpawnRunner{role: Usbip, vm_id: sys--usbipd, …}` | backend: scoped host-root carve-out with `CAP_NET_RAW`; proxy: empty | **per env** - two runners per USBIP-enabled env (`vm_id` = `sys--usbipd`, roles `backend` and `proxy`) | `d2b device usb attach --apply` dispatches `UsbipBind` with a bundle-resolved bind intent first so broker allowlist validation and the per-busid lock succeed before any listener is exposed, then applies `UsbipBindFirewallRule`, ensures the per-env backend (`usbipd -4 --tcp-port `) and bounded proxy (`socat TCP-LISTEN:3240,bind=,fork,max-children=4,reuseaddr ...`) are spawned and TCP-ready, then runs `UsbipProxyReconcile`. Host kernel module is `usbip-host` (not `vhci_hcd`, which is the guest module). | -| `CloudHypervisor` (guest-control VM) | `d2b@.service` runner; replaced by broker `SpawnRunner{role: CloudHypervisor, vm_id: , …}` | empty | per VM | On a guest-control-enabled VM the broker, as a SpawnRunner **side-effect**, grants the unprivileged `d2bd` daemon uid a minimal ACL on the per-VM vsock transport so the daemon can run the authenticated readiness/config-read bridge: traversal `--x` on every non-public component of the per-VM state dir and `rw` on `vsock.sock`. The grant is scoped to the **current** socket inode/dev (the broker re-fstats after the fd-based setfacl and aborts+retries if the path is replaced mid-grant), grants **only** that single daemon uid (no `g:`, no default, no blanket entry), and retries until bound while cloud-hypervisor finishes creating the socket. It runs as a revoke-then-grant at each cloud-hypervisor (re-)spawn - any stale daemon grant on a replaced/disabled socket inode is revoked first - so a disabled or replaced socket cannot retain a stale daemon grant; a dedicated stop-time teardown revoke hook is future work (`SignalRunner` carries no socket path). The shared traversal `--x` grants on non-public ancestors are retained, since the daemon also needs them for the per-VM api-socket and sibling VMs depend on them. Both grant and revoke emit hash-only audit records (`target_class` ∈ {`state-dir`, `ancestor`, `vsock-socket`}, `daemon_principal`, `acl_diff_hash`, `result`); raw socket / state-dir paths are never recorded. A **second** SpawnRunner side-effect grants the cloud-hypervisor **runner** uid connect access to the cross-principal `d2b-gctl` token fs-share socket (served by the narrower `gctlfs` principal, ADR 0021, so cloud-hypervisor does not own it as it owns its other fs-share sockets): `--x` traversal on the per-VM `guest-control` dir and `rw` with an explicit `m::rw` mask on the `gctlfs`-owned `d2b-gctl` socket inode. The explicit mask lifts the 0700 socket's `mask::---`, which otherwise masks out the inherited `default:u:` grant and EACCESes cloud-hypervisor's vhost-user connect (hanging device-init at api-ready timeout). It is `(dev, ino)`-scoped (re-fstat after the fd-based setfacl), grants **only** the runner uid (no execute in the mask, no group/other broadening), and retries until the socket is bound. It emits its own hash-only audit record (`target_class` ∈ {`gctlfs-dir`, `gctlfs-socket`}, `consumer_principal` = `cloud-hypervisor-runner`, `acl_diff_hash`, `result`); raw paths and uids-by-value are never recorded. | +| `Usbip` | per-env singletons `d2b-sys--usbipd-{proxy,backend}.{service,socket}`; replaced by broker `SpawnRunner{role: Usbip, vm_id: sys--usbipd, …}` | backend: scoped host-root carve-out with `CAP_NET_RAW`; proxy: empty | **per env** - two runners per USBIP-enabled env (`vm_id` = `sys--usbipd`, roles `backend` and `proxy`) | The host runner contract remains defined here, but non-qemu `d2b device usb attach|detach --apply` currently fails closed before `UsbipBind`, `UsbipBindFirewallRule`, proxy reconciliation, or claim release because the signed target-local USBIP Process/ComponentSession effect path is unavailable. Host kernel module is `usbip-host` (not `vhci_hcd`, which is the guest module). | +| `CloudHypervisor` (NixOS VM) | `d2b@.service` runner; replaced by broker `SpawnRunner{role: CloudHypervisor, vm_id: , ...}` | empty | per VM | The broker validates the signed runner intent and the enrolled ComponentSession endpoint before the daemon or target-local Process can use it; no token share or fallback transport is present. | ### Metrics transport @@ -569,7 +545,7 @@ This section is the canonical legacy-systemd obituary index. The canonical surface is exactly: - `d2bd.service` (unprivileged daemon) -- `d2b-priv-broker.service` + `d2b-priv-broker.socket` +- `d2b-broker.service` + `d2b-broker.socket` (socket-activated privileged broker) - per-VM / per-role runners spawned via broker `SpawnRunner` (no systemd unit per runner; lifecycle is daemon-supervised via pidfd) @@ -616,7 +592,7 @@ is advisory until the fixture-contract lane is enabled and promoted. | Per-VM `desktopItems` generation (cli.nix `d2b-launch-`) | Daemon-native launcher module emitting `.desktop` wrappers calling `d2b guest start --apply`. | not emitted | The root-visible `systemd.services.*` declarations the framework owns -under `nixos-modules/` are `d2bd`, `d2b-priv-broker`, +under `nixos-modules/` are `d2bd`, `d2b-broker`, `d2b-load-store-db` (boot-time tmpfiles helper), and `d2b-load-host-keys` (boot-time helper). The observability backend declares native services inside the auto-declared `sys-obs` VM, not @@ -647,21 +623,17 @@ is in the machine-readable public-operation matrix Notes: -- `sync` is **admin-only**: it dispatches the daemon's `ReadGuestConfig` - verb, which is gated to the `d2b-admin` role at `SO_PEERCRED` - accept time (`verb_requires_admin("readGuestConfig")`). A - launcher-role caller is rejected with the typed `authz-not-admin` - error (exit `75`) before any guest read. The local review sub-verbs - (`diff` / `approve` / `reject` / `status`) operate on the host-side - staging copy and dispatch no daemon verb, so a launcher can run +- `sync` is **admin-only**: it dispatches the ConfigNixos service's + `ReadGuestConfig` operation, which is gated to the `d2b-admin` role at + `SO_PEERCRED` accept time. A launcher-role caller is rejected with the + typed `authz-not-admin` error (exit `75`) before any guest read. The local + review sub-verbs (`diff` / `approve` / `reject` / `status`) operate on the + host-side staging copy and dispatch no daemon verb, so a launcher can run them - which is why the schema models the `config` group as - `d2b-launcher` + `d2b-admin`. `sync` reads the guest-editable - config over the authenticated **guest-control** vsock (the daemon's - `ReadGuestConfig` → `ReadGuestFile` path), not over SSH, and writes - only into the host-side staging copy; it never evaluates or imports - the guest bytes. It fails **closed**: a VM whose running generation - predates guest-control (or that does not advertise `ReadGuestFile`) - returns a typed error rather than silently falling back to SSH. + `d2b-launcher` + `d2b-admin`. `sync` reads the guest-editable config through + the authenticated ConfigNixos ComponentSession service and writes only into + the host-side staging copy; it never evaluates or imports the guest bytes. + It fails closed when the target-local service or session is unavailable. - `approve` / `reject` are the trust transition and are **host-operator-only** in the handler (the guest can never approve its own config); they write only the operator-named `--to` path. @@ -675,7 +647,7 @@ The `d2b exec run` verb is a **public** operation handled entirely by the unprivileged daemon and CLI. It dispatches **no** broker request (`brokerRequired: false`): the daemon holds an in-process exec **session table** and proxies typed exec -ops over the authenticated guest-control vsock to the VM's `guestd`. It +ops over the authenticated ComponentSession to the target-local Process. It is in the machine-readable public-operation matrix ([`schemas/v2/privileges.json`](schemas/v2/privileges.json), `publicOperations[].operation = "exec"`). @@ -689,7 +661,7 @@ Notes: - `exec` is **admin-only**: the daemon checks the `SO_PEERCRED` admin role on the owner connection **before** any session lookup, slot reservation, transport connect, or `ExecCreate`. A launcher-role - caller is rejected first, with no guest-control session established. + caller is rejected first, with no ComponentSession established. - The daemon emits **one** kind=critical session-establishment audit event per exec session carrying only redacted fields: `vm`, `peer_uid`, and the negotiated `tty` flag. The opaque `session_handle`, the guest @@ -707,7 +679,8 @@ Notes: The `d2b shell` verb is a **public** operation handled by the unprivileged daemon and CLI. It dispatches **no** broker request (`brokerRequired: false`): the daemon proxies persistent-shell management and -terminal operations over the authenticated guest-control channel to guestd. It +terminal operations over the authenticated ComponentSession to the target-local +shell Process. It is in the machine-readable public-operation matrix ([`schemas/v2/privileges.json`](schemas/v2/privileges.json), `publicOperations[].operation = "shell"`). @@ -719,12 +692,12 @@ is in the machine-readable public-operation matrix Notes: - `shell` is **admin-only**: the daemon checks the `SO_PEERCRED` admin role - before guest-control contact or owner-session handoff. + before ComponentSession contact or owner-session handoff. - The daemon records attach, detach, and kill decisions in its daemon-owned JSONL event stream. Records carry only the VM name, peer uid, closed action/result enums, force flag when relevant, and a fixed shell correlation digest. - Raw shell names, terminal session handles, terminal bytes, helper diagnostics, - argv, env, paths, and guest-control nonces never appear in metric labels, + argv, env, paths, and ComponentSession evidence never appear in metric labels, traces, or daemon audit fields. - No host mutation flows through the broker for `shell`, so there is no `OpAuditRecord`; the broker remains uninvolved. @@ -759,7 +732,7 @@ capabilities. `CAP_SYS_PTRACE` is explicitly excluded. ### Socket-activation contract -`d2b-priv-broker.socket` is socket-activated. systemd creates, +`d2b-broker.socket` is socket-activated. systemd creates, binds, listens on, and applies the ACL to the socket before the broker process starts. The broker adopts the fd via `SD_LISTEN_FDS`. @@ -773,7 +746,7 @@ process starts. The broker adopts the fd via `SD_LISTEN_FDS`. - The broker calls `sd_notify(READY=1)` only after the listener fd is adopted and the audit log is open - the systemd `notify` service type guarantees daemon readiness. -- `d2bd.service` carries `Wants=d2b-priv-broker.socket` (not +- `d2bd.service` carries `Wants=d2b-broker.socket` (not `Requires=`) so the daemon can serve even when the broker has idled. - The socket is the **broker private socket** at `/run/d2b/priv.sock`, owned `root:d2bd` with mode @@ -850,7 +823,7 @@ write live-host validation evidence. | --- | --- | --- | --- | --- | | `cloud-hypervisor` | `vm--cloud-hypervisor` | `CAP_NET_ADMIN` (transient - runner drops it after the SCM_RIGHTS tap-fd recv path before entering its main loop; static minijail allowlist cannot express "transient", so the profile declares the setup-time union) | `/dev/kvm`, `/dev/net/tun` (optional `/dev/dri/renderD128` + `/dev/nvidia0` when graphics/accelerator passthrough is bound to this runner) | owner-local broker and provider tests | | `qemu-media` | `vm--qemu-media` | empty | fd-backed QMP/media runner: read-only root + read-only `/nix/store`, private PID/mount namespaces, masked `/dev`, no `/dev/bus/usb`, `/dev/net/tun`, or `/dev/vhost-net` path exposure, no media path binds, and writable access only to `/run/d2b/vms/` plus the per-VM state dir. `/dev/kvm` is classified as the only declared device class for focused ACL/fd handoff; vhost-net remains inherited-fd only. Focused Wayland/GTK display access uses `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, and host-session socket ACLs, not broad path binds. `seccompPolicyRef = w1-qemu-media`; no-new-privileges is installed by broker spawn before seccomp. | `tests/unit/nix/cases/external-vm-kind.nix` + owner-local broker/provider tests | -| `virtiofsd` | `vm--virtiofsd-` | empty | ADR 0021 broker-pre-established single-entry user namespace; `requiresStartRoot = false`, zero host capabilities, `--sandbox=chroot --inode-file-handles=never`. Normal shares map to `d2b--runner`; `d2b-gctl` maps to `d2b--gctlfs` and is read-only. | owner-local broker and provider tests | +| `virtiofsd` | `vm--virtiofsd-` | empty | ADR 0021 broker-pre-established single-entry user namespace; `requiresStartRoot = false`, zero host capabilities, `--sandbox=chroot --inode-file-handles=never`. Normal shares map to the declared Provider share and are read-only where specified. | owner-local broker and provider tests | | `swtpm` (long-lived sidecar) | `vm--swtpm` | empty | **CRITICAL** RW bind of `/var/lib/d2b/vms//swtpm` (TPM 2.0 NVRAM + EK seed) + `/run/d2b/vms//` (the TPM socket + flush socket live here as `tpm.sock` and `tpm-flush.sock` respectively) (control socket). MUST be real RW bind, NOT tmpfs. Wiping or losing the bind forces Entra/Intune re-enrollment for work-aad. | owner-local broker/provider tests + `tests/integration/live/swtpm-persistence-smoke.sh` | | `swtpm-flush` (pre-start one-shot) | `vm--swtpm-flush` | empty | Same `/var/lib/d2b/vms//swtpm` + `/run/d2b/vms//` (the TPM socket + flush socket live here as `tpm.sock` and `tpm-flush.sock` respectively) binds as the long-lived `swtpm` sidecar; runs `swtpm_ioctl -i` flush before the sidecar adopts state. | shares the swtpm validator + persistence smoke | | `gpu` | `vm--gpu` | empty (per-role smoke proves virgl/venus/cross-domain run under SCHED_OTHER) | device binds: `/dev/kvm`, `/dev/dri/renderD128`, `/dev/nvidiactl`, `/dev/nvidia0`, `/dev/nvidia-uvm`, `/dev/udmabuf`; mount `/run/user//wayland-0` → `/run/d2b-gpu//wayland-0`; ioctls: full `DRM_IOCTL_VIRTGPU_*` family (via DeviceClass::Dri) | owner-local GPU and broker tests | @@ -860,32 +833,3 @@ write live-host validation evidence. | `usbip` backend | `vm-sys--usbipd-backend` | uid 0 carve-out + `CAP_NET_RAW` | host module `usbip-host` (not `vhci_hcd`, which is the guest module); long-lived per-env `usbipd` backend. `usbipd` must write the kernel `usbip_sockfd` sysfs attribute as host-root, so the broker gives this one runner a scoped root carve-out with a private PID namespace and fresh procfs; `/etc`, `/var`, `/home`, `/root`, `/run`, `/tmp`, `/boot`, `/mnt`, `/media`, `/srv`, and `/opt` masked; `/dev` masked; and only the currently locked `/dev/bus/usb//` node(s) rebound writable. | owner-local USBIP and broker tests | | `usbip` proxy | `vm-sys--usbipd-proxy` | empty | self-binding TCP proxy from `:3240` to `127.0.0.1:`; no device access | owner-local USBIP and broker tests | | `otel-host-bridge` | (host-scoped) `d2b-otel-host-bridge` | empty (fd-only contract; no AF_VSOCK socket creation) | bind set: `/run/d2b/otel`, CH vsock host socket, `host-egress.sock` (RW listen target); broker rejects bundle intent whose source VM ≠ `observability.vmName` | owner-local observability and broker tests | - - -## U4 cutover runner admission - -`LaunchCutoverRunner` is a narrow pre-drain broker admission operation. It is -Admin-only, accepts exactly one SCM_RIGHTS bootstrap descriptor, and resolves -the runner executable from trusted broker configuration. It is not a -`SpawnRunner` uid-0 exception, does not place the child in a per-VM unit, and -does not authorize any cutover effect by itself. The adapted runner peer may -use only `CutoverAudit` and `CutoverEffect`; both operations bind the -operation and capability digests, and the broker reloads the current bundle -before resolving trusted runner or activation intents. Audit responses are -returned only after the broker audit file and directory are durable. -The launch request binds the transferred capability digest in the broker's -operation registry; later runner requests with a missing, replayed, or -mismatched or expired digest are refused, including after a broker restart. -After the HostDrain effect succeeds, the broker enters a persisted -cutover-window allowlist: only that operation's runner may call -`CutoverAudit` or `CutoverEffect`; ordinary daemon operations are denied until -the window is closed. Quarantine, finalization, and durable-Volume destruction -consume marker-bound opaque payloads; they never accept generic delete or raw -path requests. -Recursive removal is descriptor-anchored: it refuses symlinks, foreign owners, -hardlinks, special files, and device changes, and uses `unlinkat` against the -opened operation-owned directory. - -## Related ADRs - -- [ADR 0015: daemon-only clean break](../adr/0015-daemon-only-clean-break.md) - the architectural decision record that defines the daemon-only root surface of `d2bd` + `d2b-priv-broker`. diff --git a/docs/reference/provider-capability-matrix.md b/docs/reference/provider-capability-matrix.md index 290b01c2d9..853265e4f6 100644 --- a/docs/reference/provider-capability-matrix.md +++ b/docs/reference/provider-capability-matrix.md @@ -14,11 +14,11 @@ For display and virtual I/O capabilities beyond console and audio, see ## Providers in scope -| Provider | Identity | Guest-control channel | +| Provider | Identity | ComponentSession channel | | --- | --- | --- | -| Cloud Hypervisor NixOS | Local VM managed by `d2bd` + `d2b-priv-broker` | `guestd` over authenticated vsock guest-control | -| qemu-media | Dedicated media/console workload; no `guestd` | None | -| ACA sandbox | Provider-managed workload (Azure Container Apps) | guestd-compatible in-sandbox agent over ADR 0032 relay/peer transport | +| Cloud Hypervisor NixOS | Local VM managed by `d2bd` + `d2b-broker` | `target-local Process` over authenticated vsock component-session | +| qemu-media | Dedicated media/console workload; no `target-local Process` | None | +| ACA sandbox | Provider-managed workload (Azure Container Apps) | target-local Process-compatible in-sandbox agent over ADR 0032 relay/peer transport | --- @@ -28,7 +28,7 @@ For display and virtual I/O capabilities beyond console and audio, see | --- | --- | --- | --- | --- | | Cloud Hypervisor NixOS | ✓ | Broker-owned `--serial` backend; attach-safe and non-blocking. | Daemon-side ring-buffer drainer; broker or broker-spawned component owns the fd. | See [Console transport - Cloud Hypervisor](#console-transport--cloud-hypervisor). | | qemu-media | ✓ | Broker-owned fd-backed chardev (PTY/fd-store design, not a qemu-created path socket). | Same daemon ring-buffer contract; broker-owned fd. | Qemu path sockets weaken permission posture; the fd-backed design is the posture baseline. See [Console transport - qemu-media](#console-transport--qemu-media). | -| ACA sandbox | ✓ via guestd | Provider guestd terminal/console over ADR 0032 relay/peer transport. | Not applicable (provider-managed draining). | Missing guestd is provider misconfiguration; see [ACA console - provider misconfiguration](#aca-console--provider-misconfiguration). | +| ACA sandbox | ✓ via target-local Process | Provider target-local Process terminal/console over ADR 0032 relay/peer transport. | Not applicable (provider-managed draining). | Missing target-local Process is provider misconfiguration; see [ACA console - provider misconfiguration](#aca-console--provider-misconfiguration). | ### Console transport - Cloud Hypervisor @@ -50,7 +50,7 @@ console fd during a `d2bd` restart so draining is not interrupted. ### Console transport - qemu-media -qemu-media VMs do not run `guestd`. The daemon accesses the console +qemu-media VMs do not run `target-local Process`. The daemon accesses the console through a broker-owned fd-backed chardev. The broker opens a socketpair (or PTY master) and passes the relevant fd to QEMU at launch time using the `chardev socket,fd=N` mechanism rather than a @@ -72,7 +72,7 @@ contract is identical to the Cloud Hypervisor case. ### ACA console - provider misconfiguration -ACA sandboxes are expected to run a guestd-compatible in-sandbox +ACA sandboxes are expected to run a target-local Process-compatible in-sandbox agent. If the agent is absent, the daemon returns a typed `provider-misconfigured` error with a remediation that points to the sandbox configuration. The daemon does **not** fall back to @@ -90,7 +90,7 @@ control traffic. | Transport | Isolation mechanism | | --- | --- | -| Local vsock (Cloud Hypervisor, qemu-media) | Separate vsock port per stream type. Console and guestd control (health, audio, exec) must use distinct vsock CID/port pairs. Multiplexing console data and control RPCs over the same vsock connection is forbidden. virtio-vsock per-connection flow control is credit-based; independent connections do not share backpressure. | +| Local vsock (Cloud Hypervisor, qemu-media) | Separate vsock port per stream type. Console and target-local Process control (health, audio, exec) must use distinct vsock CID/port pairs. Multiplexing console data and control RPCs over the same vsock connection is forbidden. virtio-vsock per-connection flow control is credit-based; independent connections do not share backpressure. | | ADR 0032 relay/peer (ACA) | Dedicated logical channel or stream within the relay transport for console bytes, separated from health-check pings, audio policy RPCs, and other control traffic. The relay transport must not share backpressure state between the console stream and control queues. Where the relay protocol supports per-stream priority, the console stream is assigned lower priority than health and control messages. | **Ring-buffer backpressure contract**: The daemon-side ring buffer is @@ -112,14 +112,14 @@ not a holder of the console fd. | Provider | Host audio enforcement | Guest audio enforcement | Offline audio policy | Notes | | --- | --- | --- | --- | --- | -| Cloud Hypervisor NixOS | ✓ PipeWire/vhost-user-sound controller | ✓ via `guestd` over authenticated guest-control | N/A (live state) | Reports `enforcement: host-and-guest` when both sides apply; host-side `off` is fail-closed; see [Audio enforcement - Cloud Hypervisor](#audio-enforcement--cloud-hypervisor). | +| Cloud Hypervisor NixOS | ✓ PipeWire/vhost-user-sound controller | ✓ via `target-local Process` over authenticated component-session | N/A (live state) | Reports `enforcement: host-and-guest` when both sides apply; host-side `off` is fail-closed; see [Audio enforcement - Cloud Hypervisor](#audio-enforcement--cloud-hypervisor). | | qemu-media | ✓ host/qemu audio subset when declared | `unsupported` - `enforcement: unsupported` reported for guest-side capability | ✓ Persisted offline policy | See [Audio enforcement - qemu-media](#audio-enforcement--qemu-media). | -| ACA sandbox | None (no local host PipeWire nodes or broker mutations) | ✓ remote guestd policy only | None | Reports `enforcement: guest-only` when guestd applies; no local audio state files or broker host mutations for ACA sandboxes; see [ACA audio](#aca-audio). | +| ACA sandbox | None (no local host PipeWire nodes or broker mutations) | ✓ remote target-local Process policy only | None | Reports `enforcement: guest-only` when target-local Process applies; no local audio state files or broker host mutations for ACA sandboxes; see [ACA audio](#aca-audio). | ### Audio enforcement - Cloud Hypervisor Cloud Hypervisor NixOS VMs support both host-side PipeWire enforcement -and guest-side enforcement via `guestd`: +and guest-side enforcement via `target-local Process`: - Volume and gain are bounded `0..=100` domain values validated at the public-wire boundary before reaching the daemon. @@ -145,7 +145,7 @@ and guest-side enforcement via `guestd`: declared VM name set, which is validated at eval time (`^[a-z][a-z0-9-]*$`). - Host-side `off` requests are fail-closed: the host boundary is sealed - even when `guestd` is unresponsive; the response carries a degraded + even when `target-local Process` is unresponsive; the response carries a degraded result for the guest-side enforcement step so the operator knows the guest-side did not apply. - Successful host and guest application reports `enforcement: @@ -155,7 +155,7 @@ and guest-side enforcement via `guestd`: ### Audio enforcement - qemu-media -qemu-media VMs do not run `guestd`. The daemon: +qemu-media VMs do not run `target-local Process`. The daemon: - applies the declared host/qemu audio subset when it is advertised in the qemu-media capability declaration; @@ -169,13 +169,13 @@ treat it as an error. ### ACA audio -ACA sandboxes use remote guestd audio policy only. The host does not +ACA sandboxes use remote target-local Process audio policy only. The host does not create local audio state files, PipeWire nodes, vhost-user-sound -connections, or broker host mutations for ACA targets. Missing guestd +connections, or broker host mutations for ACA targets. Missing target-local Process on an ACA sandbox is provider misconfiguration and surfaces as a typed error with remediation, not a silent no-op. -Successful ACA guestd application reports `enforcement: guest-only` in +Successful ACA target-local Process application reports `enforcement: guest-only` in `audio status` output. --- @@ -195,7 +195,7 @@ provider: - qemu-media host-side audio controls are enabled when the host subset is supported; the UI shows a `host-only` annotation alongside the controls. -- ACA missing-guestd states surface as `provider-misconfigured` errors +- ACA missing-target-local Process states surface as `provider-misconfigured` errors in `AudioVmError.kind` with remediation text, not as disabled UI controls. - Volume and gain sliders send final or debounced mutations only; they do not dismiss layer-shell popups during drag. diff --git a/docs/reference/provider-managed-sandboxes.md b/docs/reference/provider-managed-sandboxes.md index ef1f449bee..600d8a4768 100644 --- a/docs/reference/provider-managed-sandboxes.md +++ b/docs/reference/provider-managed-sandboxes.md @@ -13,7 +13,7 @@ capabilities, rate-limit/backoff/circuit behavior, credential boundary, safe diagnostics rules, and error shapes for provider-managed sandboxes. For nodes where d2b owns the full host stack (hypervisor, broker, -guest-control), see [remote full-host nodes](./remote-full-host-nodes.md). +ComponentSession), see [remote full-host nodes](./remote-full-host-nodes.md). --- @@ -21,15 +21,15 @@ guest-control), see [remote full-host nodes](./remote-full-host-nodes.md). A provider-managed sandbox is a named workload target in a realm whose lifecycle (create, start, stop, destroy) is owned by an external provider -API rather than by a `d2b-priv-broker` running on a locally managed +API rather than by a `d2b-broker` running on a locally managed host. From the daemon's perspective it is a node with a bounded positive capability set derived from what that provider API or provider-side d2b agent supports. The daemon never provisions, registers, or expects a full -host `d2bd`, `d2b-priv-broker`, KVM subsystem, vsock channel, cgroup +host `d2bd`, `d2b-broker`, KVM subsystem, vsock channel, cgroup subtree, namespace hierarchy, full-host lifecycle, or device-hotplug surface on a provider-managed node. ADR 0039 defines one exception to the old exec-only model: a provider-managed sandbox may advertise persistent shell -only when it runs a guestd-compatible d2b agent that exposes shell +only when it runs a provider-agent-compatible d2b agent that exposes shell control and terminal-v1 streams over the constellation peer transport. This model is distinct from a **remote full-host node** (see @@ -40,9 +40,9 @@ key differences: | Dimension | Provider-managed sandbox | Remote full-host node | | --- | --- | --- | -| Who owns lifecycle | Cloud provider API | Remote `d2bd` + `d2b-priv-broker` | +| Who owns lifecycle | Cloud provider API | Remote `d2bd` + `d2b-broker` | | Broker presence | None | Full broker on the remote host | -| Guest-control / vsock | No vsock or raw guest-control tunnel; persistent-shell-capable sandboxes require a guestd-compatible agent over constellation peer transport. | Present | +| ComponentSession / vsock | No vsock or raw ComponentSession tunnel; persistent-shell-capable sandboxes require a provider-agent-compatible agent over constellation peer transport. | Present | | KVM / hypervisor | Absent | Present | | Cgroup / namespace authority | Absent | Present (remote host) | | systemd | Absent | Present (remote host) | @@ -63,13 +63,13 @@ table is not supported; operations requiring it receive | Capability | Azure Container Apps support | Notes | | --- | --- | --- | | `lifecycle` | Conditional | Advertised only when sandbox defaults are configured; create/start/stop/list map to the Azure Container Apps sandbox data plane. | -| `exec` | ✓ | Synchronous Azure Container Apps `executeShellCommand`; returns a derived execution id, not a durable guest-control session. | -| `persistent-shell` | No | Live ADR 0039 capability. The executeShellCommand-only adapter must not advertise persistent shell; support requires a guestd-compatible in-sandbox agent over the constellation peer transport. | +| `exec` | ✓ | Synchronous Azure Container Apps `executeShellCommand`; returns a derived execution id, not a durable ComponentSession session. | +| `persistent-shell` | No | Live ADR 0039 capability. The executeShellCommand-only adapter must not advertise persistent shell; support requires a provider-agent-compatible in-sandbox agent over the constellation peer transport. | | `logs` | ✗ | No retained-log stream in this adapter. | | `pty` | ✗ | No interactive TTY or stdio attachment. | | `file-copy` | ✗ | No bounded file-copy API. | | `port-forward` | ✗ | No generic tunnel or port-forward API. | -| `vsock` | ✗ | No guest-control vsock channel. | +| `vsock` | ✗ | No ComponentSession vsock channel. | | `virtiofs` | ✗ | No per-workload /nix/store hardlink farm or virtiofsd share. | | `window-forwarding` / `display-streaming` | ✗ | No Wayland/virtio-gpu or video sidecar. | | `clipboard` | ✗ | No clipboard bridge. | @@ -105,7 +105,7 @@ provider itself uses deterministic workload labels to discover upstream state before creating or retrying mutating lifecycle calls. Persistent shell support is a separate provider trait surface. A -guestd-compatible sandbox that advertises `persistent-shell` handles +provider-agent-compatible sandbox that advertises `persistent-shell` handles `ShellList`, `ShellAttach`, `ShellDetach`, and `ShellKill` through a `PersistentShellProvider`-style seam and binds attach to an authorized `shell-pty` stream. This seam is not `WorkloadProvider::exec`, not durable @@ -115,30 +115,30 @@ typed capability denials for `Shell*` operations. --- -## guestd-compatible provider-agent bootstrap contract +## Provider-agent bootstrap contract A provider-managed sandbox may advertise `persistent-shell` only after its -provider reports a complete, non-secret guestd-compatible bootstrap contract: +provider reports a complete, non-secret provider-agent-compatible bootstrap contract: -1. The sandbox image places the guestd-compatible d2b agent binary in the +1. The sandbox image places the provider-agent-compatible d2b agent binary in the image under provider control. 2. Auth bootstrap material is short-lived and relay-scoped; long-lived realm, provider, and Relay rule credentials remain gateway-side only. 3. The agent learns only an ADR 0032 peer-transport rendezvous, not a raw - guest-control/vsock endpoint and not a provider-specific shell channel. + ComponentSession/vsock endpoint and not a provider-specific shell channel. 4. The sandbox has a workload identity suitable for acquiring its scoped relay sender material. 5. The persistent-shell helper is available in the sandbox image. 6. The agent reports bounded effective shell limits (`maxSessions` 1-256, `maxAttached` 1-64, and `maxAttached <= maxSessions`). 7. Health and capability advertisement come from the in-sandbox agent, with - generation metadata for the guest boot, guestd instance, and shell daemon. + generation metadata for the guest boot, provider-agent instance, and shell daemon. If any prerequisite is absent or malformed, the provider advertises no `persistent-shell` capability. The current Azure Container Apps `executeShellCommand` adapter intentionally reports the fail-closed bootstrap shape: it may advertise `exec` and provider-managed isolation, but it does not -claim persistent shell until an ACA image can boot the guestd-compatible agent. +claim persistent shell until an ACA image can boot the provider-agent-compatible agent. --- @@ -149,17 +149,17 @@ are never routed through this adapter. Requests for them fail closed with `UnsupportedFeature` or `CapabilityDenied`; there are no fallbacks. - **No broker operation forwarding.** The adapter never forwards raw - `d2b-priv-broker` frames to the container runtime. -- **No raw guest-control or vsock frames.** The current - executeShellCommand-only Azure Container Apps adapter has no guestd instance + `d2b-broker` frames to the container runtime. +- **No raw ComponentSession or vsock frames.** The current + executeShellCommand-only Azure Container Apps adapter has no provider-agent instance and no vsock channel to attach or tunnel. Future persistent-shell-capable - provider sandboxes must use a guestd-compatible d2b agent over the ADR - 0032 peer transport; they still do not expose raw guest-control frames or a + provider sandboxes must use a provider-agent-compatible d2b agent over the ADR + 0032 peer transport; they still do not expose raw ComponentSession frames or a provider-specific shell channel. - **No exec-to-shell fallback.** `executeShellCommand`, `WorkloadProvider::exec`, and durable execution are one-shot/durable exec surfaces, not ADR 0039 persistent shell. A sandbox without a - guestd-compatible agent and `persistent-shell` capability refuses `Shell*` + provider-agent-compatible agent and `persistent-shell` capability refuses `Shell*` operations with `CapabilityDenied` or `UnsupportedFeature`. - **No pidfd or fd passing.** No file descriptors are exchanged with the container runtime. @@ -322,7 +322,7 @@ unchanged; retry hint fields remain internal. | `circuit-open` | `Backpressure` | Circuit breaker is open for this upstream; message includes remaining open duration. | Wait for the duration in the error message before retrying. | | `credential-acquisition-failed` | `AuthenticationFailed` | The gateway could not acquire a managed/workload identity token. | Verify explicit managed/workload identity configuration. | | `upstream-authorization-failed` | `Unauthorized` | Azure Container Apps returned 403 for an otherwise formed request. | Verify the managed identity has the required Azure Container Apps data-plane role. | -| `unsupported-operation` | `UnsupportedFeature` | The operation kind is outside the Azure Container Apps adapter's scope. | Use a full-host node for operations requiring broker/guest-control/exec. | +| `unsupported-operation` | `UnsupportedFeature` | The operation kind is outside the Azure Container Apps adapter's scope. | Use a full-host node for operations requiring broker/ComponentSession/exec. | | `capability-denied` | `CapabilityDenied` | The required capability is absent from the adapter's capability set. | See the capability matrix above. | | `provider-unavailable` | `ProviderAllocationFailed` | Azure Container Apps management API is unreachable or returned an unrecoverable 5xx. | Check provider status and retry after the circuit window if one is reported. | @@ -338,7 +338,7 @@ adapter: - Interactive exec sessions or attached TTY to running containers. - Persistent named shell sessions; ADR 0039 defines this for a - guestd-compatible in-sandbox agent and explicitly excludes mapping it to + provider-agent-compatible in-sandbox agent and explicitly excludes mapping it to `executeShellCommand`. The pure provider trait/DTO seam exists, but the Azure Container Apps runtime adapter does not implement it. - Live stdio streaming (current support is polling-based log read only). @@ -359,7 +359,7 @@ the capability matrix above. - [ADR 0039 - constellation persistent shell routing](../adr/0039-constellation-persistent-shell-routing.md) - the live core contract for persistent shells on remote/provider targets. - [Remote full-host nodes](./remote-full-host-nodes.md) - the model - for nodes that run their own `d2bd`/broker/guest-control stack. + for nodes that run their own `d2bd`/broker/ComponentSession stack. - [Azure Relay transport](./transport-azure-relay.md) - the Relay WebSocket transport used for sandbox sender connections. - [Realm core](./realm-core.md) - typed error shapes, diff --git a/docs/reference/qemu-media.md b/docs/reference/qemu-media.md index 4d03114935..2299af4014 100644 --- a/docs/reference/qemu-media.md +++ b/docs/reference/qemu-media.md @@ -5,7 +5,7 @@ `runtime.kind = "qemu-media"` declares a manually started local QEMU VM for external media workflows. It uses d2b's daemon/broker control plane, but not the per-VM NixOS evaluator, Cloud Hypervisor, store -sync, guest-control, SSH, or in-guest observability paths. +sync, component-session, SSH, or in-guest observability paths. For the general VM runtime selection policy, including why qemu-media is not selected as a full VM runtime, see @@ -21,7 +21,7 @@ not selected as a full VM runtime, see | Boot media | After the runner is alive, `d2bd` asks the broker to run `QemuMediaBoot`; the broker opens the declared boot source, sends the fd to QEMU over QMP, attaches USB storage, waits for QMP success responses, then continues QEMU. | | Hotplug | `d2b device usb attach` / `detach` route to `QemuMediaAttach` / `QemuMediaDetach`, not USBIP. | | Shutdown | `d2b guest stop --apply` sends broker-mediated QMP `system_powerdown`, waits for `query-status` to report a stopped guest or the VMM pidfd to exit, then uses QMP `quit` / forced pidfd cleanup only as needed. | -| Unsupported capabilities | guest-control, exec, config-sync, SSH, store-sync, keys, and in-guest observability. | +| Unsupported capabilities | component-session, exec, config-sync, SSH, store-sync, keys, and in-guest observability. | ## Options diff --git a/docs/reference/realm-controller-config.md b/docs/reference/realm-controller-config.md index 423512d613..3804fe88a0 100644 --- a/docs/reference/realm-controller-config.md +++ b/docs/reference/realm-controller-config.md @@ -30,7 +30,7 @@ The current invariants are: | --- | --- | | `metadataOnly` | The file is descriptive metadata, not a runtime activation command. | | `noSystemdUnitsMaterialized` | Legacy double-negative kept for the v2 schema: `true` means every controller row is metadata-only with no emitted systemd unit/socket; `false` means at least one host-local realm materializes daemon/broker units or sockets. | -| `preservesGlobalDaemonBehavior` | Existing `d2bd.service`, `d2b-priv-broker.socket`, and `d2b-priv-broker.service` behavior is unchanged. | +| `preservesGlobalDaemonBehavior` | Existing `d2bd.service`, `d2b-broker.socket`, and `d2b-broker.service` behavior is unchanged. | | `preservesDirectUnixSocketSemantics` | Future realm clients are expected to authenticate to the owning realm socket directly, not through a host byte proxy. | ## Deterministic names @@ -124,8 +124,8 @@ state. `localRuntime.providers[]` copies the local runtime provider catalog entries used by the realm's workloads, including the provider id (`local-cloud-hypervisor` -or `local-qemu-media`), local driver, legacy capability summary, operation -capability summary, autostart policy, and role/service summaries. +or `local-qemu-media`), local driver, capability summary, operation capability +summary, autostart policy, and role/service summaries. `localRuntime.workloads[]` records the VM workload id, VM name, owning env, runtime metadata, and the preserved host paths: @@ -135,7 +135,7 @@ runtime metadata, and the preserved host paths: | `paths.stateDir` | Existing per-VM persistent state root, such as `/var/lib/d2b/vms/`. | | `paths.runDir` | Existing per-VM runtime directory under `/run/d2b/vms/`. | | `paths.storeView` | Existing per-VM store-view hardlink farm root. | -| `paths.guestControlDir` | Existing guest-control socket directory. | +| `paths.componentSessionDir` | Existing ComponentSession socket directory. | The `localRuntime.invariants` block must keep `metadataOnly`, `existingGlobalVmPathsPreserved`, `noStateMigrationDuringActivation`, and diff --git a/docs/reference/remote-full-host-nodes.md b/docs/reference/remote-full-host-nodes.md index 8402c6a8ac..cafe094233 100644 --- a/docs/reference/remote-full-host-nodes.md +++ b/docs/reference/remote-full-host-nodes.md @@ -22,7 +22,7 @@ substrate capabilities see [host substrate providers](./host-substrate-providers The architectural rationale is in [ADR 0032](../adr/0032-d2b-v2-constellation-control-plane.md). For provider-managed sandboxes - nodes whose lifecycle is owned by a -cloud provider API rather than by a locally managed `d2b-priv-broker` - +cloud provider API rather than by a locally managed `d2b-broker` - see [provider-managed sandboxes](./provider-managed-sandboxes.md). --- @@ -30,10 +30,10 @@ see [provider-managed sandboxes](./provider-managed-sandboxes.md). ## What a remote full-host node is A remote full-host node is a host running its own `d2bd`, -`d2b-priv-broker`, and guest-control stack that a gateway guest +`d2b-broker`, and component-session stack that a gateway guest can reach through a transport peer session. From the gateway's point of view, the remote host appears as a named `NodeId` in a realm with -a declared capability set. All lifecycle, broker, and guest-control +a declared capability set. All lifecycle, broker, and component-session work executes on the remote host itself; the gateway routes typed operation requests and receives typed responses. @@ -124,7 +124,7 @@ A node that has not advertised `logs` cannot receive retained-log requests. `persistent-shell`. Remote full-host nodes advertise it only after their daemon implements the shell operation family. Shell routing reaches the remote daemon as semantic `Shell*` operations and the remote daemon re-originates local -guest-control shell RPCs near the guest. +component-session shell RPCs near the guest. All `Shell*` remote operations require an explicit workload target. A shell request without `workload` fails at route preflight with `missing-workload` before any remote peer can observe it. @@ -151,7 +151,7 @@ established transport session. Routing proceeds as follows: client. The adapter never exposes a raw byte/frame tunnel to callers. 5. The remote node's local `d2bd` receives the operation request, enforces its own realm/capability policy, and invokes the local - broker or guest-control path. + broker or component-session path. 6. The remote node returns a semantic response to the gateway. 7. The gateway records the result in dedup state and returns the response to the requesting peer. @@ -159,7 +159,7 @@ established transport session. Routing proceeds as follows: The gateway never forwards: - raw broker operation frames or payloads; -- guest-control frames or vsock data; +- component-session frames or vsock data; - pidfds or file descriptors; - host paths, endpoint strings, or socket addresses; - relay, provider, or realm credentials; @@ -169,7 +169,7 @@ The gateway never forwards: authz envelope. The remote host re-originates all side effects through its own -`d2bd`/broker/guest-control stack. The gateway is a routing +`d2bd`/broker/component-session stack. The gateway is a routing intermediary, not an execution proxy. --- @@ -336,12 +336,12 @@ does not implement fallbacks or workarounds. | Surface | Boundary | | --- | --- | -| Raw broker operation forwarding | The gateway never forwards raw `d2b-priv-broker` frames. All broker work stays on the remote host. | -| Guest-control frame tunneling | Guest-control (vsock) frames are not proxied through the gateway. Persistent shell routes as [ADR 0039](../adr/0039-constellation-persistent-shell-routing.md) semantic `Shell*` operations; the remote `d2bd` opens its own guest-control sessions near the guest. | +| Raw broker operation forwarding | The gateway never forwards raw `d2b-broker` frames. All broker work stays on the remote host. | +| ComponentSession frame tunneling | ComponentSession frames are not proxied through the gateway. Persistent shell routes as [ADR 0039](../adr/0039-constellation-persistent-shell-routing.md) semantic `Shell*` operations; the remote `d2bd` opens its own sessions near the guest. | | Pidfd / fd forwarding | File descriptors, pidfds, and socket handles are never sent across the transport session. | | Host path and endpoint exposure | Host-local paths, socket addresses, runner argv, and endpoint strings are not visible in the operation envelope or in gateway audit records. | | Provider/relay credential forwarding | Transport and realm credentials remain in the layer that owns them and are never placed in operation payloads. | -| Remote host install / host prepare | Registration assumes the remote host is already running a compatible `d2bd`/`d2b-priv-broker` stack. Host installation and host preparation are out of scope for this adapter. | +| Remote host install / host prepare | Registration assumes the remote host is already running a compatible `d2bd`/`d2b-broker` stack. Host installation and host preparation are out of scope for this adapter. | | Production WAN transports | The preview is validated with mock and loopback peer clients only. Azure Relay over a live WAN, QUIC, and SSH are not yet connected. See [transport support policy](./transport-support-policy.md). | | Network mutation | The adapter does not configure routing, firewall rules, or overlays on the remote host or on the gateway network. | | SSH fallback | There is no implicit SSH fallback when the peer session transport is unavailable. Callers receive a typed `GatewayUnavailable` / transport-layer refusal. | @@ -361,7 +361,7 @@ only. The following items are deferred to later work: - Provider-provisioned remote hosts (see the [provider-managed sandbox](./provider-managed-sandboxes.md) model for provider-scoped work). -- Daemon, CLI, guestd, and live provider-agent runtime forwarding for +- Daemon, CLI, target-local Process, and live provider-agent runtime forwarding for persistent shell remote/provider targets ([ADR 0039](../adr/0039-constellation-persistent-shell-routing.md)). - Automatic capability refresh without re-registration. - End-user principal delegation across a gateway; the preview binds the diff --git a/docs/reference/runtime-provider-selection.md b/docs/reference/runtime-provider-selection.md index 5ab5de4282..bd11e00b65 100644 --- a/docs/reference/runtime-provider-selection.md +++ b/docs/reference/runtime-provider-selection.md @@ -4,7 +4,7 @@ Runtime providers describe the local VM runner family used for a workload. They are capability declarations and routing policy, not -escape hatches around `d2bd` or `d2b-priv-broker`. +escape hatches around `d2bd` or `d2b-broker`. ## Default provider @@ -33,7 +33,7 @@ The following ids are reserved but are not production VM runtimes: | `crosvm` | Refuses as unsupported. crosvm is currently used for selected sidecars, not as the full VM runtime. | | `qemu` | Refuses as unsupported. General QEMU VM runtime semantics are not implemented. | | `qemu-media` | Refuses as a full VM runtime. It remains the dedicated media sidecar runtime documented in [qemu-media](./qemu-media.md). | -| `firecracker` | Refuses as unsupported and, before any side effect, rejects workloads requiring guest-control, virtiofs/store sync, graphics, audio, USB, or desktop surfaces. | +| `firecracker` | Refuses as unsupported and, before any side effect, rejects workloads requiring component-session, virtiofs/store sync, graphics, audio, USB, or desktop surfaces. | Unknown provider ids fail closed with `UnsupportedFeature` and a remediation that points operators back to `local-cloud-hypervisor`. @@ -51,7 +51,7 @@ Runtime provider selection is data-driven: reason, and remediation. Features that require graphics, audio, USB, virtiofs-backed storage, -guest-control, or local GPU acceleration should assume the Cloud Hypervisor +component-session, or local GPU acceleration should assume the Cloud Hypervisor runtime unless a future provider explicitly advertises the needed capability. ## Related component requirements diff --git a/docs/reference/schemas/v1/processes.md b/docs/reference/schemas/v1/processes.md index 794d8c6865..bc2d1ddff3 100644 --- a/docs/reference/schemas/v1/processes.md +++ b/docs/reference/schemas/v1/processes.md @@ -2,6 +2,10 @@ `processes.json` is the private supervision artifact. It encodes the per-VM process DAG, typed readiness predicates, cgroup placement, restart policy, and minijail profile references that `d2bd` uses to launch and reconcile roles. +> Historical v1 schema. It is retained only for archived artifact decoding; +> v3 emits the ComponentSession Process contract and no longer emits the +> `guestSshReadiness` role. + Producer: `nixos-modules/manifest-processes.nix` emits this artifact; `packages/d2b-core` parses it. Schema: [`processes.json`](./processes.json) (forward reference; generated with `bazel run //packages/xtask:xtask -- gen-schemas`). diff --git a/docs/reference/schemas/v2/bundle.json b/docs/reference/schemas/v2/bundle.json index a604a0fa4a..77f0894eed 100644 --- a/docs/reference/schemas/v2/bundle.json +++ b/docs/reference/schemas/v2/bundle.json @@ -52,13 +52,6 @@ "$ref": "#/definitions/BundleClosureRef" } }, - "cutoverRunnerPath": { - "description": "Trusted store path for the one-shot cutover runner.", - "type": [ - "string", - "null" - ] - }, "generation": { "description": "Generation metadata for auditing drift between Nix output and runtime state.", "allOf": [ diff --git a/docs/reference/schemas/v2/d2b-realm-core.json b/docs/reference/schemas/v2/d2b-realm-core.json index 3b48738aaa..a895f989bc 100644 --- a/docs/reference/schemas/v2/d2b-realm-core.json +++ b/docs/reference/schemas/v2/d2b-realm-core.json @@ -7687,14 +7687,14 @@ "type": "object", "required": [ "guest_boot_id", - "guestd_instance_id", + "session_instance_id", "shell_daemon_instance_id" ], "properties": { "guest_boot_id": { "$ref": "#/definitions/ProtocolToken" }, - "guestd_instance_id": { + "session_instance_id": { "$ref": "#/definitions/ProtocolToken" }, "shell_daemon_instance_id": { diff --git a/docs/reference/schemas/v2/d2b-realm-core.md b/docs/reference/schemas/v2/d2b-realm-core.md index 2497691b08..ce5f82717a 100644 --- a/docs/reference/schemas/v2/d2b-realm-core.md +++ b/docs/reference/schemas/v2/d2b-realm-core.md @@ -7,7 +7,7 @@ model from `packages/d2b-realm-core`. It is generated by `bazel run //packages/xtask:xtask -- gen-schemas` from `D2bRealmCoreSchema` and should not be edited by hand. -This schema is **not** the local daemon, broker, or guest-control wire +This schema is **not** the local daemon, broker, or ComponentSession wire protocol. It documents the shared model roots used by realm target addressing, operation routing, stream authorization, redacted audit metadata, provider/workload placement, route/enrollment metadata, and typed diff --git a/docs/reference/schemas/v2/guest-control.json b/docs/reference/schemas/v2/guest-control.json deleted file mode 100644 index 7ce61010b4..0000000000 --- a/docs/reference/schemas/v2/guest-control.json +++ /dev/null @@ -1,3216 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "GuestControlSchema", - "type": "object", - "required": [ - "activateSystemStart", - "activateSystemStarted", - "activateSystemStatus", - "activateSystemStatusResult", - "audioSet", - "audioSetResult", - "audioStatus", - "audioStatusResult", - "authenticate", - "authenticated", - "capabilities", - "capabilitiesRequest", - "closeStdin", - "closeStdinResult", - "controlAck", - "error", - "execCancel", - "execCreate", - "execCreated", - "execInspect", - "execInspected", - "execList", - "execListed", - "execLogChunk", - "execLogs", - "execSignal", - "execWait", - "execWaited", - "health", - "healthRequest", - "hello", - "helloOk", - "limits", - "outputChunk", - "protocolVersion", - "readGuestFile", - "readGuestFileResult", - "readOutput", - "schemaVersion", - "shellAttach", - "shellAttached", - "shellCloseAttach", - "shellDetach", - "shellDetached", - "shellKill", - "shellKilled", - "shellList", - "shellListed", - "terminalCloseStdin", - "terminalReadOutput", - "terminalTtyWinResize", - "terminalWriteStdin", - "transport", - "ttyWinResize", - "usbipImport", - "usbipImportResult", - "usbipStatus", - "usbipStatusResult", - "writeStdin", - "writeStdinResult" - ], - "properties": { - "activateSystemStart": { - "$ref": "#/definitions/GuestActivationStartRequest" - }, - "activateSystemStarted": { - "$ref": "#/definitions/GuestActivationStartResponse" - }, - "activateSystemStatus": { - "$ref": "#/definitions/GuestActivationStatusRequest" - }, - "activateSystemStatusResult": { - "$ref": "#/definitions/GuestActivationStatusResponse" - }, - "audioSet": { - "$ref": "#/definitions/AudioSetRequest" - }, - "audioSetResult": { - "$ref": "#/definitions/AudioSetResponse" - }, - "audioStatus": { - "$ref": "#/definitions/AudioStatusRequest" - }, - "audioStatusResult": { - "$ref": "#/definitions/AudioStatusResponse" - }, - "authenticate": { - "$ref": "#/definitions/AuthenticateRequest" - }, - "authenticated": { - "$ref": "#/definitions/AuthenticateResponse" - }, - "capabilities": { - "$ref": "#/definitions/CapabilitiesResponse" - }, - "capabilitiesRequest": { - "$ref": "#/definitions/CapabilitiesRequest" - }, - "closeStdin": { - "$ref": "#/definitions/CloseStdinRequest" - }, - "closeStdinResult": { - "$ref": "#/definitions/CloseStdinResponse" - }, - "controlAck": { - "$ref": "#/definitions/ControlAck" - }, - "error": { - "$ref": "#/definitions/GuestControlError" - }, - "execCancel": { - "$ref": "#/definitions/ExecCancelRequest" - }, - "execCreate": { - "$ref": "#/definitions/ExecCreateRequest" - }, - "execCreated": { - "$ref": "#/definitions/ExecCreateResponse" - }, - "execInspect": { - "$ref": "#/definitions/ExecInspectRequest" - }, - "execInspected": { - "$ref": "#/definitions/ExecInspectResponse" - }, - "execList": { - "$ref": "#/definitions/ExecListRequest" - }, - "execListed": { - "$ref": "#/definitions/ExecListResponse" - }, - "execLogChunk": { - "$ref": "#/definitions/ExecLogsResponse" - }, - "execLogs": { - "$ref": "#/definitions/ExecLogsRequest" - }, - "execSignal": { - "$ref": "#/definitions/ExecSignalRequest" - }, - "execWait": { - "$ref": "#/definitions/ExecWaitRequest" - }, - "execWaited": { - "$ref": "#/definitions/ExecWaitResponse" - }, - "health": { - "$ref": "#/definitions/HealthResponse" - }, - "healthRequest": { - "$ref": "#/definitions/HealthRequest" - }, - "hello": { - "$ref": "#/definitions/HelloRequest" - }, - "helloOk": { - "$ref": "#/definitions/HelloResponse" - }, - "limits": { - "$ref": "#/definitions/GuestEffectiveLimits" - }, - "outputChunk": { - "$ref": "#/definitions/ReadOutputResponse" - }, - "protocolVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "readGuestFile": { - "$ref": "#/definitions/ReadGuestFileRequest" - }, - "readGuestFileResult": { - "$ref": "#/definitions/ReadGuestFileResponse" - }, - "readOutput": { - "$ref": "#/definitions/ReadOutputRequest" - }, - "schemaVersion": { - "$ref": "#/definitions/GuestSchemaVersion" - }, - "shellAttach": { - "$ref": "#/definitions/ShellAttachRequest" - }, - "shellAttached": { - "$ref": "#/definitions/ShellAttachResponse" - }, - "shellCloseAttach": { - "$ref": "#/definitions/ShellCloseAttachRequest" - }, - "shellDetach": { - "$ref": "#/definitions/ShellDetachRequest" - }, - "shellDetached": { - "$ref": "#/definitions/ShellDetachResponse" - }, - "shellKill": { - "$ref": "#/definitions/ShellKillRequest" - }, - "shellKilled": { - "$ref": "#/definitions/ShellKillResponse" - }, - "shellList": { - "$ref": "#/definitions/ShellListRequest" - }, - "shellListed": { - "$ref": "#/definitions/ShellListResponse" - }, - "terminalCloseStdin": { - "$ref": "#/definitions/TerminalCloseStdinRequest" - }, - "terminalReadOutput": { - "$ref": "#/definitions/TerminalReadOutputRequest" - }, - "terminalTtyWinResize": { - "$ref": "#/definitions/TerminalTtyWinResizeRequest" - }, - "terminalWriteStdin": { - "$ref": "#/definitions/TerminalWriteStdinRequest" - }, - "transport": { - "$ref": "#/definitions/GuestTransportSchema" - }, - "ttyWinResize": { - "$ref": "#/definitions/TtyWinResizeRequest" - }, - "usbipImport": { - "$ref": "#/definitions/UsbipImportRequest" - }, - "usbipImportResult": { - "$ref": "#/definitions/UsbipImportResponse" - }, - "usbipStatus": { - "$ref": "#/definitions/UsbipStatusRequest" - }, - "usbipStatusResult": { - "$ref": "#/definitions/UsbipStatusResponse" - }, - "writeStdin": { - "$ref": "#/definitions/WriteStdinRequest" - }, - "writeStdinResult": { - "$ref": "#/definitions/WriteStdinResponse" - } - }, - "additionalProperties": false, - "definitions": { - "ArgvSha256": { - "type": "string", - "maxLength": 64, - "minLength": 1 - }, - "AudioChannel": { - "description": "The audio channel to target.", - "type": "string", - "enum": [ - "microphone", - "speaker" - ] - }, - "AudioSetKind": { - "description": "Which audio mutation to perform.", - "oneOf": [ - { - "description": "Turn a channel on (unmute) or off (mute).", - "type": "string", - "enum": [ - "grant" - ] - }, - { - "description": "Adjust the channel level (volume/gain), `0..=100`.", - "type": "string", - "enum": [ - "level" - ] - } - ] - }, - "AudioSetRequest": { - "description": "Request: mutate one audio channel's grant or level in the guest session.", - "type": "object", - "required": [ - "channel", - "grantOn", - "kind", - "level", - "metadata" - ], - "properties": { - "channel": { - "$ref": "#/definitions/AudioChannel" - }, - "grantOn": { - "description": "For `Grant`: `true` = on, `false` = off.", - "type": "boolean" - }, - "kind": { - "$ref": "#/definitions/AudioSetKind" - }, - "level": { - "description": "For `Level`: `0..=100`; ignored for `Grant`.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "AudioSetResponse": { - "description": "Response: updated channel state after the mutation.", - "type": "object", - "properties": { - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "state": { - "anyOf": [ - { - "$ref": "#/definitions/GuestAudioChannelState" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "AudioStatusRequest": { - "description": "Request: query current guest audio policy state.", - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "AudioStatusResponse": { - "description": "Response: per-channel guest audio state.", - "type": "object", - "properties": { - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "microphone": { - "anyOf": [ - { - "$ref": "#/definitions/GuestAudioChannelState" - }, - { - "type": "null" - } - ] - }, - "speaker": { - "anyOf": [ - { - "$ref": "#/definitions/GuestAudioChannelState" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "AuthenticateRequest": { - "type": "object", - "required": [ - "guestBootId", - "guestNonce", - "hostAuthTag", - "hostNonce", - "metadata", - "transcriptVersion" - ], - "properties": { - "guestBootId": { - "$ref": "#/definitions/GuestBootId" - }, - "guestNonce": { - "$ref": "#/definitions/GuestNonce" - }, - "hostAuthTag": { - "$ref": "#/definitions/GuestAuthTag" - }, - "hostNonce": { - "$ref": "#/definitions/GuestNonce" - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "transcriptVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "AuthenticateResponse": { - "type": "object", - "properties": { - "capabilities": { - "anyOf": [ - { - "$ref": "#/definitions/CapabilitiesResponse" - }, - { - "type": "null" - } - ] - }, - "capabilitiesHash": { - "anyOf": [ - { - "$ref": "#/definitions/CapabilitiesHash" - }, - { - "type": "null" - } - ] - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "guestAuthTag": { - "anyOf": [ - { - "$ref": "#/definitions/GuestAuthTag" - }, - { - "type": "null" - } - ] - }, - "health": { - "anyOf": [ - { - "$ref": "#/definitions/HealthResponse" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "CapabilitiesHash": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "CapabilitiesRequest": { - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "CapabilitiesResponse": { - "type": "object", - "required": [ - "capabilities", - "limits", - "protocolVersion" - ], - "properties": { - "capabilities": { - "type": "array", - "items": { - "$ref": "#/definitions/GuestCapability" - }, - "maxItems": 32 - }, - "limits": { - "$ref": "#/definitions/GuestEffectiveLimits" - }, - "protocolVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "CloseStdinRequest": { - "type": "object", - "required": [ - "metadata", - "offset" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "CloseStdinResponse": { - "type": "object", - "required": [ - "disposition", - "finalOffset", - "stdinState" - ], - "properties": { - "disposition": { - "$ref": "#/definitions/WriteDisposition" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "finalOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdinState": { - "$ref": "#/definitions/StdinState" - } - }, - "additionalProperties": false - }, - "ControlAck": { - "type": "object", - "required": [ - "controlSeq", - "duplicate" - ], - "properties": { - "controlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "duplicate": { - "type": "boolean" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "EnvKey": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "EnvValue": { - "type": "string", - "maxLength": 8192, - "minLength": 1 - }, - "EnvVar": { - "type": "object", - "required": [ - "key", - "value" - ], - "properties": { - "key": { - "$ref": "#/definitions/EnvKey" - }, - "value": { - "$ref": "#/definitions/EnvValue" - } - }, - "additionalProperties": false - }, - "ExecCancelReason": { - "type": "string", - "enum": [ - "client-disconnect", - "user-requested", - "slow-consumer", - "protocol-error" - ] - }, - "ExecCancelRequest": { - "type": "object", - "required": [ - "controlSeq", - "metadata", - "reason" - ], - "properties": { - "controlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "reason": { - "$ref": "#/definitions/ExecCancelReason" - } - }, - "additionalProperties": false - }, - "ExecCreateRequest": { - "type": "object", - "required": [ - "argv", - "detached", - "env", - "metadata", - "outputPolicy", - "stdinOpen", - "tty" - ], - "properties": { - "argv": { - "type": "array", - "items": { - "$ref": "#/definitions/GuestArg" - }, - "maxItems": 128, - "minItems": 1 - }, - "cwd": { - "anyOf": [ - { - "$ref": "#/definitions/GuestCwd" - }, - { - "type": "null" - } - ] - }, - "detached": { - "type": "boolean" - }, - "env": { - "type": "array", - "items": { - "$ref": "#/definitions/EnvVar" - }, - "maxItems": 256 - }, - "initialTerminalSize": { - "anyOf": [ - { - "$ref": "#/definitions/TerminalSize" - }, - { - "type": "null" - } - ] - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "outputPolicy": { - "$ref": "#/definitions/OutputPolicy" - }, - "stdinOpen": { - "type": "boolean" - }, - "tty": { - "type": "boolean" - }, - "user": { - "anyOf": [ - { - "$ref": "#/definitions/GuestUser" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "ExecCreateResponse": { - "type": "object", - "required": [ - "controlSeq", - "createdAtMonotonicNs", - "effectiveLimits", - "state", - "stderrCursor", - "stdoutCursor" - ], - "properties": { - "controlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "createdAtMonotonicNs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "effectiveLimits": { - "$ref": "#/definitions/GuestEffectiveLimits" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "execId": { - "anyOf": [ - { - "$ref": "#/definitions/ExecId" - }, - { - "type": "null" - } - ] - }, - "state": { - "$ref": "#/definitions/ExecState" - }, - "stderrCursor": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutCursor": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "ExecId": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "ExecInspectRequest": { - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - } - }, - "additionalProperties": false - }, - "ExecInspectResponse": { - "type": "object", - "required": [ - "lastControlSeq", - "state", - "stateGeneration", - "stderrDroppedBytes", - "stderrEndOffset", - "stderrStartOffset", - "stderrTruncatedForRetention", - "stdinState", - "stdoutDroppedBytes", - "stdoutEndOffset", - "stdoutStartOffset", - "stdoutTruncatedForRetention" - ], - "properties": { - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "lastControlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "state": { - "$ref": "#/definitions/ExecState" - }, - "stateGeneration": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrDroppedBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrEndOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrStartOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrTruncatedForRetention": { - "type": "boolean" - }, - "stdinState": { - "$ref": "#/definitions/StdinState" - }, - "stdoutDroppedBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutEndOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutStartOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutTruncatedForRetention": { - "type": "boolean" - }, - "visibleTerminalStatus": { - "anyOf": [ - { - "$ref": "#/definitions/TerminalStatus" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "ExecListEntry": { - "description": "One detached exec record summary. Discloses only bounded, non-sensitive metadata: the opaque id, slot, lifecycle state, create time, an argv hash (never the raw argv), and retained-log accounting.", - "type": "object", - "required": [ - "argvSha256", - "createTimeUnix", - "droppedBytes", - "execId", - "slot", - "state", - "stderrTruncated", - "stdoutTruncated" - ], - "properties": { - "argvSha256": { - "$ref": "#/definitions/ArgvSha256" - }, - "createTimeUnix": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "droppedBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "execId": { - "$ref": "#/definitions/ExecId" - }, - "slot": { - "type": "integer", - "format": "uint32", - "maximum": 31.0, - "minimum": 0.0 - }, - "state": { - "$ref": "#/definitions/ExecState" - }, - "stderrTruncated": { - "type": "boolean" - }, - "stdoutTruncated": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ExecListRequest": { - "description": "Read-only listing of detached execs visible to the same VM token on the same guest boot. Carries no exec id, argv, or output; the boot id binds the request to the current guest generation.", - "type": "object", - "required": [ - "guestBootId", - "metadata" - ], - "properties": { - "guestBootId": { - "$ref": "#/definitions/GuestBootId" - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "ExecListResponse": { - "type": "object", - "required": [ - "entries" - ], - "properties": { - "entries": { - "type": "array", - "items": { - "$ref": "#/definitions/ExecListEntry" - }, - "maxItems": 32 - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "ExecLogsRequest": { - "type": "object", - "required": [ - "maxLen", - "metadata", - "offset", - "stream" - ], - "properties": { - "maxLen": { - "type": "integer", - "format": "uint64", - "maximum": 1048576.0, - "minimum": 1.0 - }, - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stream": { - "$ref": "#/definitions/OutputStream" - } - }, - "additionalProperties": false - }, - "ExecLogsResponse": { - "type": "object", - "required": [ - "data", - "droppedBytes", - "endOffset", - "eof", - "nextOffset", - "offset", - "startOffset", - "stream", - "truncated" - ], - "properties": { - "data": { - "$ref": "#/definitions/GuestOutputBytes" - }, - "droppedBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "endOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "eof": { - "type": "boolean" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "nextOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "startOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stream": { - "$ref": "#/definitions/OutputStream" - }, - "truncated": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ExecSignalRequest": { - "type": "object", - "required": [ - "controlSeq", - "metadata", - "signal", - "target" - ], - "properties": { - "controlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "signal": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "target": { - "$ref": "#/definitions/SignalTarget" - } - }, - "additionalProperties": false - }, - "ExecState": { - "type": "string", - "enum": [ - "created", - "running", - "exited", - "signaled", - "cancelled", - "slow-consumer-cancelled", - "protocol-error", - "lost-guestd", - "reaped" - ] - }, - "ExecWaitRequest": { - "type": "object", - "required": [ - "metadata", - "timeoutMs" - ], - "properties": { - "knownStateGeneration": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "timeoutMs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "ExecWaitResponse": { - "type": "object", - "required": [ - "state", - "stateGeneration", - "stderrEndOffset", - "stderrStartOffset", - "stdoutEndOffset", - "stdoutStartOffset", - "timedOut" - ], - "properties": { - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "state": { - "$ref": "#/definitions/ExecState" - }, - "stateGeneration": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrEndOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrStartOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutEndOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutStartOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "timedOut": { - "type": "boolean" - }, - "visibleTerminalStatus": { - "anyOf": [ - { - "$ref": "#/definitions/TerminalStatus" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "GuestActivationId": { - "type": "string", - "maxLength": 36, - "minLength": 1 - }, - "GuestActivationMode": { - "type": "string", - "enum": [ - "switch", - "boot", - "test", - "dry-activate" - ] - }, - "GuestActivationStartRequest": { - "type": "object", - "required": [ - "activationId", - "metadata", - "mode", - "switchScriptPath", - "timeoutMs" - ], - "properties": { - "activationId": { - "$ref": "#/definitions/GuestActivationId" - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "mode": { - "$ref": "#/definitions/GuestActivationMode" - }, - "switchScriptPath": { - "$ref": "#/definitions/GuestSwitchScriptPath" - }, - "timeoutMs": { - "type": "integer", - "format": "uint64", - "maximum": 3600000.0, - "minimum": 1.0 - } - }, - "additionalProperties": false - }, - "GuestActivationStartResponse": { - "type": "object", - "required": [ - "activationId", - "state" - ], - "properties": { - "activationId": { - "$ref": "#/definitions/GuestActivationId" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "state": { - "$ref": "#/definitions/GuestActivationState" - } - }, - "additionalProperties": false - }, - "GuestActivationState": { - "type": "string", - "enum": [ - "running", - "succeeded", - "failed", - "timed-out", - "lost" - ] - }, - "GuestActivationStatusRequest": { - "type": "object", - "required": [ - "activationId", - "metadata" - ], - "properties": { - "activationId": { - "$ref": "#/definitions/GuestActivationId" - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "GuestActivationStatusResponse": { - "type": "object", - "required": [ - "activationId", - "state" - ], - "properties": { - "activationId": { - "$ref": "#/definitions/GuestActivationId" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "exitCode": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "signal": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "state": { - "$ref": "#/definitions/GuestActivationState" - }, - "statusCode": { - "type": [ - "integer", - "null" - ], - "format": "int32" - } - }, - "additionalProperties": false - }, - "GuestArg": { - "type": "string", - "maxLength": 4096, - "minLength": 1 - }, - "GuestAudioChannelState": { - "description": "Current state of one audio channel as seen by the guest PipeWire session.", - "type": "object", - "required": [ - "level", - "levelKnown", - "muted" - ], - "properties": { - "level": { - "description": "Current level 0-100; 0 when not configured or unavailable.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "levelKnown": { - "description": "True when the level value is meaningful.", - "type": "boolean" - }, - "muted": { - "description": "True when the channel is muted (grant = off).", - "type": "boolean" - } - }, - "additionalProperties": false - }, - "GuestAuthTag": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "maximum": 255.0, - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 - }, - "GuestBootId": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "GuestCapability": { - "oneOf": [ - { - "type": "string", - "enum": [ - "health", - "capabilities", - "exec-attached", - "exec-detached", - "exec-tty", - "exec-logs", - "tty-resize", - "signals", - "read-guest-file", - "usbip-import", - "shell-attached", - "shell-management", - "shell-force-attach", - "usbip-status", - "system-activation" - ] - }, - { - "description": "Read current audio policy state from the guest PipeWire session. Advertised only when guestd is configured with a PipeWire control path.", - "type": "string", - "enum": [ - "audio-status" - ] - }, - { - "description": "Mutate mic/speaker grant or level in the guest PipeWire session. Advertised alongside `AudioStatus`; requires the same configuration.", - "type": "string", - "enum": [ - "audio-set" - ] - } - ] - }, - "GuestConnectAckLine": { - "type": "string", - "maxLength": 64, - "minLength": 1 - }, - "GuestConnectAckValue": { - "type": "string", - "enum": [ - "opaque-local-port" - ] - }, - "GuestConnectRequestLine": { - "type": "string", - "maxLength": 64, - "minLength": 1 - }, - "GuestControlError": { - "type": "object", - "required": [ - "kind", - "remediation" - ], - "properties": { - "kind": { - "$ref": "#/definitions/GuestControlErrorKind" - }, - "remediation": { - "$ref": "#/definitions/HealthRemediation" - }, - "retryAfterMs": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "GuestControlErrorKind": { - "oneOf": [ - { - "type": "string", - "enum": [ - "protocol-error", - "max-chunk-exceeded", - "stdin-backpressure", - "stdin-closed", - "stdin-not-open", - "stdin-closed-by-process", - "stdin-offset-mismatch", - "stdin-byte-budget-exhausted", - "offset-expired", - "offset-in-future", - "offset-exhausted", - "output-lost", - "tty-stderr-unavailable", - "tty-required", - "exec-capacity-exceeded", - "exec-attach-capacity-exceeded", - "exec-not-found", - "exec-already-exited", - "guest-exec-disabled", - "guest-exec-root-denied", - "guest-exec-user-denied", - "cwd-invalid", - "cwd-denied", - "retained-log-path-unsafe", - "retained-log-quota-exceeded", - "read-wait-capacity-exceeded", - "wait-capacity-exceeded", - "superseded-read-wait", - "rate-limited", - "request-id-conflict", - "control-seq-mismatch", - "slow-consumer-cancelled", - "stale-session", - "guest-control-unavailable-old-generation", - "auth-failed", - "transport-unreachable", - "exec-expired", - "file-not-found", - "file-too-large", - "path-unsafe", - "read-denied", - "invalid-program", - "usbip-unavailable", - "usbip-command-failed", - "usbip-invalid-bus-id", - "usbip-invalid-host", - "guest-shell-disabled", - "shell-invalid-name", - "shell-capacity-exceeded", - "shell-attach-capacity-exceeded", - "shell-not-found", - "shell-already-attached", - "shell-pool-unavailable", - "shell-daemon-epoch-mismatch", - "shell-output-gap", - "usbip-command-timeout", - "usbip-invalid-output", - "activation-invalid-id", - "activation-invalid-path", - "activation-invalid-mode", - "activation-not-found", - "activation-status-unavailable", - "activation-timed-out", - "activation-spawn-failed" - ] - }, - { - "description": "Guest PipeWire session is unavailable or not configured.", - "type": "string", - "enum": [ - "audio-pipewire-unavailable" - ] - }, - { - "description": "Requested audio channel is not recognised.", - "type": "string", - "enum": [ - "audio-channel-unknown" - ] - }, - { - "description": "Audio level value is out of the 0-100 range.", - "type": "string", - "enum": [ - "audio-level-out-of-range" - ] - }, - { - "description": "PipeWire enforcement call succeeded but the policy could not be applied.", - "type": "string", - "enum": [ - "audio-enforcement-failed" - ] - } - ] - }, - "GuestCwd": { - "type": "string", - "maxLength": 4096, - "minLength": 1 - }, - "GuestEffectiveLimits": { - "type": "object", - "required": [ - "attachedSessionsPerVm", - "decodedWriteStdinBytesPerConnection", - "detachedStderrLogBytes", - "detachedStdoutLogBytes", - "execSessionsPerVm", - "longPollTimeoutMs", - "maxChunkBytes", - "maxRecvMessageBytes", - "pendingExecWaitsPerVm", - "pendingReadOutputWaitsPerStream", - "rpcRatePerConnectionPerSecond", - "rpcRatePerVmBurst", - "shellAttachedSessionsPerVm", - "shellSessionsPerVm", - "slowConsumerGraceMs", - "stderrLiveBufferBytes", - "stdinQueueChunksPerExec", - "stdoutLiveBufferBytes", - "writeStdinHandlersPerConnection" - ], - "properties": { - "attachedSessionsPerVm": { - "type": "integer", - "format": "uint32", - "maximum": 64.0, - "minimum": 0.0 - }, - "decodedWriteStdinBytesPerConnection": { - "type": "integer", - "format": "uint64", - "maximum": 16777216.0, - "minimum": 0.0 - }, - "detachedStderrLogBytes": { - "type": "integer", - "format": "uint64", - "maximum": 4194304.0, - "minimum": 0.0 - }, - "detachedStdoutLogBytes": { - "type": "integer", - "format": "uint64", - "maximum": 4194304.0, - "minimum": 0.0 - }, - "execSessionsPerVm": { - "type": "integer", - "format": "uint32", - "maximum": 256.0, - "minimum": 0.0 - }, - "longPollTimeoutMs": { - "type": "integer", - "format": "uint64", - "maximum": 1000.0, - "minimum": 0.0 - }, - "maxChunkBytes": { - "type": "integer", - "format": "uint64", - "maximum": 1048576.0, - "minimum": 1.0 - }, - "maxRecvMessageBytes": { - "type": "integer", - "format": "uint64", - "maximum": 4194304.0, - "minimum": 1.0 - }, - "pendingExecWaitsPerVm": { - "type": "integer", - "format": "uint32", - "maximum": 512.0, - "minimum": 0.0 - }, - "pendingReadOutputWaitsPerStream": { - "type": "integer", - "format": "uint32", - "maximum": 512.0, - "minimum": 0.0 - }, - "rpcRatePerConnectionPerSecond": { - "type": "integer", - "format": "uint32", - "maximum": 200.0, - "minimum": 0.0 - }, - "rpcRatePerVmBurst": { - "type": "integer", - "format": "uint32", - "maximum": 1000.0, - "minimum": 0.0 - }, - "shellAttachedSessionsPerVm": { - "type": "integer", - "format": "uint32", - "maximum": 64.0, - "minimum": 0.0 - }, - "shellSessionsPerVm": { - "type": "integer", - "format": "uint32", - "maximum": 256.0, - "minimum": 0.0 - }, - "slowConsumerGraceMs": { - "type": "integer", - "format": "uint64", - "maximum": 300000.0, - "minimum": 0.0 - }, - "stderrLiveBufferBytes": { - "type": "integer", - "format": "uint64", - "maximum": 8388608.0, - "minimum": 0.0 - }, - "stdinQueueChunksPerExec": { - "type": "integer", - "format": "uint32", - "maximum": 1.0, - "minimum": 0.0 - }, - "stdoutLiveBufferBytes": { - "type": "integer", - "format": "uint64", - "maximum": 8388608.0, - "minimum": 0.0 - }, - "writeStdinHandlersPerConnection": { - "type": "integer", - "format": "uint32", - "maximum": 4.0, - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "GuestExecRequestMetadata": { - "type": "object", - "required": [ - "common", - "execId", - "guestBootId" - ], - "properties": { - "common": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "execId": { - "$ref": "#/definitions/ExecId" - }, - "guestBootId": { - "$ref": "#/definitions/GuestBootId" - } - }, - "additionalProperties": false - }, - "GuestFileBytes": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "maximum": 255.0, - "minimum": 0.0 - }, - "maxItems": 524288, - "minItems": 0 - }, - "GuestFileId": { - "description": "Closed set of host-declared guest files readable via `ReadGuestFile`. Currently ships only `GuestConfig` (the in-guest editable config working copy).", - "type": "string", - "enum": [ - "guest-config" - ] - }, - "GuestHostConnectShape": { - "type": "object", - "required": [ - "ackValue", - "okAck", - "requestLine" - ], - "properties": { - "ackValue": { - "$ref": "#/definitions/GuestConnectAckValue" - }, - "okAck": { - "$ref": "#/definitions/GuestConnectAckLine" - }, - "requestLine": { - "$ref": "#/definitions/GuestConnectRequestLine" - } - }, - "additionalProperties": false - }, - "GuestIdentityBinding": { - "type": "string", - "enum": [ - "vm-id-cid-port-and-token-transcript" - ] - }, - "GuestNonce": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "maximum": 255.0, - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 - }, - "GuestOutputBytes": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "maximum": 255.0, - "minimum": 0.0 - }, - "maxItems": 1048576, - "minItems": 0 - }, - "GuestReadinessContract": { - "type": "object", - "required": [ - "preTtrpcFailuresAreHostSynthesized", - "requiresConnectHelloAuthAndHealth", - "socketExistenceIsReadiness" - ], - "properties": { - "preTtrpcFailuresAreHostSynthesized": { - "type": "boolean" - }, - "requiresConnectHelloAuthAndHealth": { - "type": "boolean" - }, - "socketExistenceIsReadiness": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "GuestRequestMetadata": { - "type": "object", - "required": [ - "protocolVersion", - "requestId", - "vmId" - ], - "properties": { - "protocolVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "requestId": { - "$ref": "#/definitions/RequestId" - }, - "vmId": { - "$ref": "#/definitions/GuestVmId" - } - }, - "additionalProperties": false - }, - "GuestSchemaVersion": { - "type": "string", - "maxLength": 32, - "minLength": 1 - }, - "GuestStdinBytes": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "maximum": 255.0, - "minimum": 0.0 - }, - "maxItems": 1048576, - "minItems": 1 - }, - "GuestSubsystem": { - "type": "string", - "enum": [ - "guestd", - "userd", - "exec", - "log-storage", - "token", - "vsock", - "usbip", - "shell", - "shpool", - "system-activation", - "audio" - ] - }, - "GuestSwitchScriptPath": { - "type": "string", - "maxLength": 4096, - "minLength": 1 - }, - "GuestTransportKind": { - "type": "string", - "enum": [ - "virtio-vsock-ttrpc" - ] - }, - "GuestTransportSchema": { - "type": "object", - "required": [ - "direction", - "guestControlVsockPort", - "guestToHostObservabilityPort", - "hostConnect", - "identityBinding", - "readiness", - "reservedSideChannelPort", - "transport", - "ttrpcFrameCapBytes" - ], - "properties": { - "direction": { - "$ref": "#/definitions/GuestVsockDirection" - }, - "guestControlVsockPort": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "guestToHostObservabilityPort": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "hostConnect": { - "$ref": "#/definitions/GuestHostConnectShape" - }, - "identityBinding": { - "$ref": "#/definitions/GuestIdentityBinding" - }, - "readiness": { - "$ref": "#/definitions/GuestReadinessContract" - }, - "reservedSideChannelPort": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "transport": { - "$ref": "#/definitions/GuestTransportKind" - }, - "ttrpcFrameCapBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "GuestUsbipBusId": { - "type": "string", - "maxLength": 31, - "minLength": 1 - }, - "GuestUsbipHost": { - "type": "string", - "maxLength": 64, - "minLength": 1 - }, - "GuestUser": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "GuestVmId": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "GuestVsockDirection": { - "type": "string", - "enum": [ - "host-to-guest" - ] - }, - "HealthOrigin": { - "type": "string", - "enum": [ - "guest-reported", - "host-synthesized" - ] - }, - "HealthReason": { - "type": "string", - "enum": [ - "none", - "old-generation", - "listener-absent", - "connect-refused", - "connect-timeout", - "eof-before-ack", - "malformed-ack", - "ack-too-long", - "transport-io", - "auth-token-rejected", - "protocol-version-unsupported", - "session-generation-mismatch", - "exec-subsystem-unavailable", - "log-storage-unavailable", - "quota-exceeded", - "rate-limited", - "internal-health-check-failed" - ] - }, - "HealthRemediation": { - "type": "string", - "enum": [ - "none", - "retry", - "restart-vm", - "upgrade-guest", - "check-auth-token", - "check-guestd-service", - "reduce-load", - "inspect-guest-logs" - ] - }, - "HealthRequest": { - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "HealthResponse": { - "type": "object", - "required": [ - "capabilities", - "degradedSubsystems", - "origin", - "protocolVersion", - "reason", - "remediation", - "state" - ], - "properties": { - "capabilities": { - "type": "array", - "items": { - "$ref": "#/definitions/GuestCapability" - }, - "maxItems": 32 - }, - "degradedSubsystems": { - "type": "array", - "items": { - "$ref": "#/definitions/GuestSubsystem" - }, - "maxItems": 16 - }, - "origin": { - "$ref": "#/definitions/HealthOrigin" - }, - "protocolVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "reason": { - "$ref": "#/definitions/HealthReason" - }, - "remediation": { - "$ref": "#/definitions/HealthRemediation" - }, - "state": { - "$ref": "#/definitions/HealthState" - } - }, - "additionalProperties": false - }, - "HealthState": { - "type": "string", - "enum": [ - "healthy", - "degraded", - "unavailable-old-generation", - "listener-absent", - "transport-unreachable", - "auth-failed", - "protocol-mismatch", - "stale-session" - ] - }, - "HelloRequest": { - "type": "object", - "required": [ - "hostNonce", - "metadata", - "transcriptVersion" - ], - "properties": { - "hostNonce": { - "$ref": "#/definitions/GuestNonce" - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "transcriptVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "HelloResponse": { - "type": "object", - "required": [ - "guestBootId", - "guestNonce", - "protocolVersion" - ], - "properties": { - "guestBootId": { - "$ref": "#/definitions/GuestBootId" - }, - "guestNonce": { - "$ref": "#/definitions/GuestNonce" - }, - "protocolVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "OutputPolicy": { - "type": "object", - "required": [ - "maxChunkBytes", - "maxStderrLogBytes", - "maxStdoutLogBytes", - "slowConsumerTimeoutMs", - "waitTimeoutMs" - ], - "properties": { - "maxChunkBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "maxStderrLogBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "maxStdoutLogBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "slowConsumerTimeoutMs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "waitTimeoutMs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "OutputStream": { - "type": "string", - "enum": [ - "stdout", - "stderr" - ] - }, - "ReadGuestFileRequest": { - "description": "Framework read of a host-declared guest file, keyed by a closed enum (NOT a free-form path). guestd maps the key to the host-declared target.", - "type": "object", - "required": [ - "fileId", - "metadata" - ], - "properties": { - "fileId": { - "$ref": "#/definitions/GuestFileId" - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "ReadGuestFileResponse": { - "description": "Single-shot bounded response. `content` is capped at `READ_GUEST_FILE_MAX_BYTES`; oversize files fail with `FileTooLarge` BEFORE any read/allocation. `size_bytes`/`sha256` are guest-reported diagnostics only - the host recomputes both from the received bytes.", - "type": "object", - "required": [ - "content", - "fileId", - "sha256", - "sizeBytes" - ], - "properties": { - "content": { - "$ref": "#/definitions/GuestFileBytes" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "fileId": { - "$ref": "#/definitions/GuestFileId" - }, - "sha256": { - "$ref": "#/definitions/ArgvSha256" - }, - "sizeBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "ReadOutputRequest": { - "type": "object", - "required": [ - "maxLen", - "metadata", - "offset", - "stream", - "timeoutMs", - "wait" - ], - "properties": { - "maxLen": { - "type": "integer", - "format": "uint64", - "maximum": 1048576.0, - "minimum": 1.0 - }, - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stream": { - "$ref": "#/definitions/OutputStream" - }, - "timeoutMs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "wait": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ReadOutputResponse": { - "type": "object", - "required": [ - "data", - "droppedBytes", - "endOffset", - "eof", - "nextOffset", - "offset", - "startOffset", - "stream", - "timedOut", - "truncated" - ], - "properties": { - "data": { - "$ref": "#/definitions/GuestOutputBytes" - }, - "droppedBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "endOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "eof": { - "type": "boolean" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "nextOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "startOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stream": { - "$ref": "#/definitions/OutputStream" - }, - "timedOut": { - "type": "boolean" - }, - "truncated": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "RequestId": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "ShellAttachRequest": { - "type": "object", - "required": [ - "force", - "metadata" - ], - "properties": { - "force": { - "type": "boolean" - }, - "initialTerminalSize": { - "anyOf": [ - { - "$ref": "#/definitions/TerminalSize" - }, - { - "type": "null" - } - ] - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "name": { - "anyOf": [ - { - "$ref": "#/definitions/ShellName" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "ShellAttachResponse": { - "type": "object", - "required": [ - "controlSeq", - "effectiveLimits", - "forceEvicted", - "outputCursor", - "resolvedName", - "state" - ], - "properties": { - "controlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "effectiveLimits": { - "$ref": "#/definitions/GuestEffectiveLimits" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "forceEvicted": { - "type": "boolean" - }, - "outputCursor": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "resolvedName": { - "$ref": "#/definitions/ShellName" - }, - "sessionId": { - "anyOf": [ - { - "$ref": "#/definitions/TerminalSessionId" - }, - { - "type": "null" - } - ] - }, - "state": { - "$ref": "#/definitions/ShellState" - } - }, - "additionalProperties": false - }, - "ShellCloseAttachRequest": { - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/TerminalRequestMetadata" - } - }, - "additionalProperties": false - }, - "ShellCloseCause": { - "type": "string", - "enum": [ - "client-detach", - "evicted-by-force", - "evicted-by-admin-detach", - "killed-by-admin", - "pool-unavailable", - "output-gap" - ] - }, - "ShellDetachRequest": { - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "name": { - "anyOf": [ - { - "$ref": "#/definitions/ShellName" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "ShellDetachResponse": { - "type": "object", - "required": [ - "cause", - "detached", - "resolvedName" - ], - "properties": { - "cause": { - "$ref": "#/definitions/ShellCloseCause" - }, - "detached": { - "type": "boolean" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "resolvedName": { - "$ref": "#/definitions/ShellName" - } - }, - "additionalProperties": false - }, - "ShellKillRequest": { - "type": "object", - "required": [ - "metadata", - "name" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "name": { - "$ref": "#/definitions/ShellName" - } - }, - "additionalProperties": false - }, - "ShellKillResponse": { - "type": "object", - "required": [ - "killed", - "name", - "state" - ], - "properties": { - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "killed": { - "type": "boolean" - }, - "name": { - "$ref": "#/definitions/ShellName" - }, - "state": { - "$ref": "#/definitions/ShellState" - } - }, - "additionalProperties": false - }, - "ShellListEntry": { - "type": "object", - "required": [ - "attached", - "isDefault", - "name", - "state" - ], - "properties": { - "attached": { - "type": "boolean" - }, - "isDefault": { - "type": "boolean" - }, - "name": { - "$ref": "#/definitions/ShellName" - }, - "state": { - "$ref": "#/definitions/ShellState" - } - }, - "additionalProperties": false - }, - "ShellListRequest": { - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "ShellListResponse": { - "type": "object", - "required": [ - "defaultName", - "sessions" - ], - "properties": { - "defaultName": { - "$ref": "#/definitions/ShellName" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "sessions": { - "type": "array", - "items": { - "$ref": "#/definitions/ShellListEntry" - }, - "maxItems": 256 - } - }, - "additionalProperties": false - }, - "ShellName": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$" - }, - "ShellState": { - "type": "string", - "enum": [ - "attached", - "detached", - "killed", - "pool-unavailable", - "feature-disabled", - "output-gap" - ] - }, - "SignalTarget": { - "type": "string", - "enum": [ - "foreground-process-group", - "process-tree" - ] - }, - "StdinState": { - "type": "string", - "enum": [ - "open", - "closing", - "closed", - "closed-by-process", - "rejected-not-interactive" - ] - }, - "TerminalCloseStdinRequest": { - "type": "object", - "required": [ - "metadata", - "offset" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/TerminalRequestMetadata" - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "TerminalKind": { - "type": "string", - "enum": [ - "exec", - "shell" - ] - }, - "TerminalReadOutputRequest": { - "type": "object", - "required": [ - "maxLen", - "metadata", - "offset", - "stream", - "timeoutMs", - "wait" - ], - "properties": { - "maxLen": { - "type": "integer", - "format": "uint64", - "maximum": 1048576.0, - "minimum": 1.0 - }, - "metadata": { - "$ref": "#/definitions/TerminalRequestMetadata" - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stream": { - "$ref": "#/definitions/OutputStream" - }, - "timeoutMs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "wait": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "TerminalRequestMetadata": { - "type": "object", - "required": [ - "common", - "guestBootId", - "kind", - "sessionId" - ], - "properties": { - "common": { - "$ref": "#/definitions/GuestRequestMetadata" - }, - "guestBootId": { - "$ref": "#/definitions/GuestBootId" - }, - "kind": { - "$ref": "#/definitions/TerminalKind" - }, - "sessionId": { - "$ref": "#/definitions/TerminalSessionId" - } - }, - "additionalProperties": false - }, - "TerminalSessionId": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "TerminalSize": { - "type": "object", - "required": [ - "cols", - "rows" - ], - "properties": { - "cols": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 1.0 - }, - "rows": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 1.0 - } - }, - "additionalProperties": false - }, - "TerminalStatus": { - "oneOf": [ - { - "type": "object", - "required": [ - "exitCode", - "outcome" - ], - "properties": { - "exitCode": { - "type": "integer", - "format": "int32" - }, - "outcome": { - "type": "string", - "enum": [ - "exit-code" - ] - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "outcome", - "signal" - ], - "properties": { - "outcome": { - "type": "string", - "enum": [ - "signal" - ] - }, - "signal": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "outcome", - "statusCode" - ], - "properties": { - "outcome": { - "type": "string", - "enum": [ - "status-code" - ] - }, - "statusCode": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "error", - "outcome" - ], - "properties": { - "error": { - "$ref": "#/definitions/GuestControlErrorKind" - }, - "outcome": { - "type": "string", - "enum": [ - "error" - ] - } - }, - "additionalProperties": false - } - ] - }, - "TerminalTtyWinResizeRequest": { - "type": "object", - "required": [ - "cols", - "controlSeq", - "metadata", - "rows" - ], - "properties": { - "cols": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 1.0 - }, - "controlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "metadata": { - "$ref": "#/definitions/TerminalRequestMetadata" - }, - "rows": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 1.0 - } - }, - "additionalProperties": false - }, - "TerminalWriteStdinRequest": { - "type": "object", - "required": [ - "closeAfter", - "data", - "metadata", - "offset" - ], - "properties": { - "clientDeadlineMs": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "closeAfter": { - "type": "boolean" - }, - "data": { - "$ref": "#/definitions/GuestStdinBytes" - }, - "metadata": { - "$ref": "#/definitions/TerminalRequestMetadata" - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "TtyWinResizeRequest": { - "type": "object", - "required": [ - "cols", - "controlSeq", - "metadata", - "rows" - ], - "properties": { - "cols": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 1.0 - }, - "controlSeq": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "rows": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 1.0 - } - }, - "additionalProperties": false - }, - "UsbipImportAction": { - "type": "string", - "enum": [ - "attach", - "detach" - ] - }, - "UsbipImportRequest": { - "description": "Guest-side USBIP lifecycle operation. `host` and `bus_id` are host-declared values resolved by d2bd from the trusted manifest; guestd validates both before invoking `usbip`.", - "type": "object", - "required": [ - "action", - "busId", - "host", - "metadata" - ], - "properties": { - "action": { - "$ref": "#/definitions/UsbipImportAction" - }, - "busId": { - "$ref": "#/definitions/GuestUsbipBusId" - }, - "host": { - "$ref": "#/definitions/GuestUsbipHost" - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "UsbipImportResponse": { - "type": "object", - "required": [ - "action", - "busId", - "detachedPorts" - ], - "properties": { - "action": { - "$ref": "#/definitions/UsbipImportAction" - }, - "busId": { - "$ref": "#/definitions/GuestUsbipBusId" - }, - "detachedPorts": { - "type": "integer", - "format": "uint32", - "maximum": 32.0, - "minimum": 0.0 - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "UsbipStatusEntry": { - "type": "object", - "required": [ - "busId", - "host", - "port", - "tcpPort" - ], - "properties": { - "busId": { - "$ref": "#/definitions/GuestUsbipBusId" - }, - "host": { - "$ref": "#/definitions/GuestUsbipHost" - }, - "port": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 0.0 - }, - "tcpPort": { - "type": "integer", - "format": "uint32", - "maximum": 65535.0, - "minimum": 1.0 - } - }, - "additionalProperties": false - }, - "UsbipStatusRequest": { - "description": "Side-effect-free guest USBIP import observation. Optional filters are validated with the same closed host/bus-id rules as `UsbipImport`.", - "type": "object", - "required": [ - "metadata" - ], - "properties": { - "busId": { - "anyOf": [ - { - "$ref": "#/definitions/GuestUsbipBusId" - }, - { - "type": "null" - } - ] - }, - "host": { - "anyOf": [ - { - "$ref": "#/definitions/GuestUsbipHost" - }, - { - "type": "null" - } - ] - }, - "metadata": { - "$ref": "#/definitions/GuestRequestMetadata" - } - }, - "additionalProperties": false - }, - "UsbipStatusResponse": { - "type": "object", - "required": [ - "imports" - ], - "properties": { - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "imports": { - "type": "array", - "items": { - "$ref": "#/definitions/UsbipStatusEntry" - }, - "maxItems": 64 - } - }, - "additionalProperties": false - }, - "WriteDisposition": { - "type": "string", - "enum": [ - "accepted", - "duplicate", - "rejected" - ] - }, - "WriteStdinRequest": { - "type": "object", - "required": [ - "closeAfter", - "data", - "metadata", - "offset" - ], - "properties": { - "clientDeadlineMs": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "closeAfter": { - "type": "boolean" - }, - "data": { - "$ref": "#/definitions/GuestStdinBytes" - }, - "metadata": { - "$ref": "#/definitions/GuestExecRequestMetadata" - }, - "offset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "WriteStdinResponse": { - "type": "object", - "required": [ - "acceptedLen", - "acceptedOffset", - "blockedMs", - "disposition", - "nextOffset", - "stdinState" - ], - "properties": { - "acceptedLen": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "acceptedOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "blockedMs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "disposition": { - "$ref": "#/definitions/WriteDisposition" - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/GuestControlError" - }, - { - "type": "null" - } - ] - }, - "nextOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdinState": { - "$ref": "#/definitions/StdinState" - } - }, - "additionalProperties": false - } - } -} diff --git a/docs/reference/schemas/v2/guest-control.md b/docs/reference/schemas/v2/guest-control.md deleted file mode 100644 index 28de3b57b8..0000000000 --- a/docs/reference/schemas/v2/guest-control.md +++ /dev/null @@ -1,74 +0,0 @@ -# `guest-control.json` schema (`v2` companion) - -Schema: [`guest-control.json`](./guest-control.json) - -`guest-control.json` is the typed guest-control contract for ADR 0028's -ttRPC/protobuf surface. It snapshots the guest daemon control messages, health -states, exec lifecycle messages, and chunked stdio RPC shapes that host -`d2bd` and guest `d2b-guestd` must keep aligned. The protobuf service -source lives at -[`packages/d2b-contracts-control/guest_control.proto`](../../../../packages/d2b-contracts-control/guest_control.proto). -The generated Rust bindings are intentionally split by role: -`packages/d2b-contracts-control/src/generated/guest_control.rs` is message-only and -`ttrpc`-free, while `packages/d2b-guestd/src/generated/guest_control_ttrpc.rs` -contains the guestd-local service bindings. - -## Top-level sections - -- `schemaVersion` / `protocolVersion` - guest-control schema and wire version. -- `transport` - host-to-guest virtio-vsock ttRPC transport on port `14318`, - the separate guest-to-host observability port `14317`, the reserved unused - side-channel port `14319`, the Cloud Hypervisor `CONNECT 14318\n` handshake, - the bounded opaque `OK \n` acknowledgement, and the - readiness rule that socket existence alone is never readiness. -- `limits` - bounded frame, chunk, live-buffer, detached-log, and concurrency - limits exposed only after authentication through `Authenticate` / - `Capabilities`. -- `hello` - unauthenticated challenge/discovery only. It returns a guest nonce, - guest boot id, and protocol version; it does not return health, capabilities, - or a capability fingerprint. -- `authenticate` / `authenticated` - proof-of-possession boundary for the - guest-control token. `Authenticate` carries the host HMAC over the canonical - transcript; `authenticated` returns the guest HMAC plus bounded authenticated - health and capabilities. -- `healthRequest`, `health`, `capabilitiesRequest`, and `capabilities` - - authenticated readiness/version/capability RPCs. Pre-ttRPC CONNECT, Hello - challenge, and Authenticate failures are host-synthesized status, not - guest-returned `Health` RPC payloads. -- `readGuestFile` / `readGuestFileResult` - the authenticated host↔guest - framework config-read RPC that grounds `d2b config sync` for - guest-control VMs (the daemon's `ReadGuestConfig` bridge wraps it). - `ReadGuestFileRequest` keys a **closed** `GuestFileId` enum (currently - only `guest-config`, the in-guest editable config working copy) - never - a free-form path; guestd maps the key to the host-declared target. The - single-shot `ReadGuestFileResponse` returns bounded `content` (capped at - `READ_GUEST_FILE_MAX_BYTES`; oversize files fail `FileTooLarge` BEFORE - any read/allocation), plus guest-reported `sizeBytes`/`sha256` - diagnostics that the host recomputes from the received bytes, and an - optional closed-enum `GuestControlError`. It is only reachable over the - authenticated channel after `Authenticate` succeeds. -- `usbipImport` / `usbipImportResult` - the authenticated guest-side USBIP - import lifecycle RPC that grounds `d2b usb attach|detach`. Requests carry - a closed action (`attach`/`detach`), a daemon-resolved USBIP backend host, and - a validated sysfs bus id; guestd runs `usbip port`/`detach`/`attach` inside - the VM and returns only bounded status fields plus an optional closed-enum - `GuestControlError`. -- `exec*`, `writeStdin`, `readOutput`, `closeStdin`, `ttyWinResize`, and - signal/cancel messages - Docker-like exec lifecycle and chunked stdio. - `controlAck` is the shared response for resize, signal, and cancel control - events. - -## Contract notes - -- The schema describes the protocol contract; it is not a commitment to use - JSON as the runtime transport. -- Raw `argv`, environment values, stdin, stdout, and stderr bytes are - sensitivity-bearing payload fields. Implementations must project only - bounded enum/counter fields into logs, audit records, metrics, spans, health, - and user-facing errors. -- Optional protobuf scalar/string fields are optional because absence changes - behavior: `user`, `cwd`, `execId`, `knownStateGeneration`, - `clientDeadlineMs`, and `retryAfterMs` must not be collapsed into default - zero or empty-string sentinels. -- Any wire-breaking change belongs in a new schema version and matching ADR / - reference documentation update. diff --git a/docs/reference/schemas/v2/host.json b/docs/reference/schemas/v2/host.json index dc24af041c..45932b40e5 100644 --- a/docs/reference/schemas/v2/host.json +++ b/docs/reference/schemas/v2/host.json @@ -1104,7 +1104,6 @@ "configSync", "display", "exec", - "guestControl", "inGuestObservability", "keys", "lifecycle", @@ -1122,9 +1121,6 @@ "exec": { "type": "boolean" }, - "guestControl": { - "type": "boolean" - }, "inGuestObservability": { "type": "boolean" }, @@ -1177,7 +1173,6 @@ "required": [ "configSync", "exec", - "guestControl", "inGuestObservability", "keys", "ssh" @@ -1189,9 +1184,6 @@ "exec": { "type": "boolean" }, - "guestControl": { - "type": "boolean" - }, "inGuestObservability": { "type": "boolean" }, @@ -1377,7 +1369,7 @@ "audio", "video", "network", - "guest-control", + "component-session", "usb", "observability" ] diff --git a/docs/reference/schemas/v2/manifest_v04.json b/docs/reference/schemas/v2/manifest_v04.json index f36aa3808a..06c169cbfd 100644 --- a/docs/reference/schemas/v2/manifest_v04.json +++ b/docs/reference/schemas/v2/manifest_v04.json @@ -103,7 +103,6 @@ "configSync", "display", "exec", - "guestControl", "inGuestObservability", "keys", "lifecycle", @@ -121,9 +120,6 @@ "exec": { "type": "boolean" }, - "guestControl": { - "type": "boolean" - }, "inGuestObservability": { "type": "boolean" }, @@ -176,7 +172,6 @@ "required": [ "configSync", "exec", - "guestControl", "inGuestObservability", "keys", "ssh" @@ -188,9 +183,6 @@ "exec": { "type": "boolean" }, - "guestControl": { - "type": "boolean" - }, "inGuestObservability": { "type": "boolean" }, @@ -376,7 +368,7 @@ "audio", "video", "network", - "guest-control", + "component-session", "usb", "observability" ] diff --git a/docs/reference/schemas/v2/manifest_v04.md b/docs/reference/schemas/v2/manifest_v04.md index 84c6c94e47..5bac9e0b01 100644 --- a/docs/reference/schemas/v2/manifest_v04.md +++ b/docs/reference/schemas/v2/manifest_v04.md @@ -21,4 +21,4 @@ the Rust CLI, daemon, and broker. bump and matching schema update. - Per-VM rows carry runtime/provider metadata. Provider-specific socket and vsock fields may be null for local providers that do not expose the - corresponding Cloud Hypervisor or guest-control artifact. + corresponding Cloud Hypervisor or ComponentSession artifact. diff --git a/docs/reference/schemas/v2/privileges.json b/docs/reference/schemas/v2/privileges.json index 5223b52e41..c32c04333e 100644 --- a/docs/reference/schemas/v2/privileges.json +++ b/docs/reference/schemas/v2/privileges.json @@ -155,18 +155,14 @@ "CreateOrReconcileUsersGroups", "CreatePersistentTap", "CreateTapFd", - "CutoverAudit", - "CutoverEffect", "DelegateCgroupV2", "DeleteBridge", "DeletePersistentTap", "DeregisterRunnerPidfd", "DiskInit", "ExportBrokerAudit", - "GuestControlSign", "Hello", "InjectSecretById", - "LaunchCutoverRunner", "LaunchMinijailChild", "MigrateLegacySwtpmState", "ModprobeIfAllowed", @@ -259,12 +255,10 @@ "guest", "hello", "host check", - "host cutover", "host destroy --apply", "host destroy --dry-run", "host doctor --read-only", "host install", - "host migrate-storage --apply", "host prepare", "host prepare --apply", "host prepare --dry-run", diff --git a/docs/reference/schemas/v2/privileges.md b/docs/reference/schemas/v2/privileges.md index 3d0419e1c5..8748fa6a55 100644 --- a/docs/reference/schemas/v2/privileges.md +++ b/docs/reference/schemas/v2/privileges.md @@ -9,7 +9,7 @@ surface and the private broker operations. - `schemaVersion` - schema directory/version for this artifact. - `publicOperations` - public command rows (`list`, `status`, `host check`, - `host cutover`, `vm start --dry-run`, and so on). + `vm start --dry-run`, and so on). - `brokerOperations` - private broker rows (`ValidateBundle`, `RunActivation`, `SpawnRunner`, `DelegateCgroupV2`, and friends). diff --git a/docs/reference/schemas/v2/processes.json b/docs/reference/schemas/v2/processes.json index 41578d32b7..dadc992537 100644 --- a/docs/reference/schemas/v2/processes.json +++ b/docs/reference/schemas/v2/processes.json @@ -8,17 +8,6 @@ "vms" ], "properties": { - "cutoverRunner": { - "description": "Optional out-of-band host cutover runner contract.", - "anyOf": [ - { - "$ref": "#/definitions/CutoverRunnerProcess" - }, - { - "type": "null" - } - ] - }, "schemaVersion": { "description": "Schema version used by this artifact.", "type": "string" @@ -86,35 +75,6 @@ "minLength": 1, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$" }, - "CutoverRunnerProcess": { - "description": "Private process contract for the one-shot cutover runner.", - "type": "object", - "required": [ - "binaryPath", - "cgroupPlacement", - "persistent", - "singleBootstrapFd" - ], - "properties": { - "binaryPath": { - "description": "Trusted store path installed by the host-tool emitter.", - "type": "string" - }, - "cgroupPlacement": { - "description": "Closed placement label proving it is outside the VM slice.", - "type": "string" - }, - "persistent": { - "description": "The runner is never a declared persistent unit.", - "type": "boolean" - }, - "singleBootstrapFd": { - "description": "The broker transfers exactly one bootstrap descriptor.", - "type": "boolean" - } - }, - "additionalProperties": false - }, "DagEdge": { "description": "Directed dependency edge in the per-VM DAG.", "type": "object", @@ -357,7 +317,7 @@ ] }, "executionRef": { - "description": "Canonical Host or Guest execution target for this trusted runner.\n\nOmitted by legacy bundles, which resolve to `Guest/`.", + "description": "Canonical Host or Guest execution target for this trusted runner.\n\nOmitted by legacy bundles, which resolve to the singleton Host target except for the Guest-local activation runner.", "type": [ "string", "null" @@ -502,31 +462,31 @@ ] }, { - "description": "vsock relay sidecar.", + "description": "Target-local one-shot NixOS activation runner.\n\nThis role is emitted only for the Guest execution target. The activation Provider creates an EphemeralProcess resource and the target-local process Provider resolves this role from the trusted bundle.", "type": "string", "enum": [ - "vsock-relay" + "activation-nixos-runner" ] }, { - "description": "Host-to-observability-VM OTLP bridge.", + "description": "vsock relay sidecar.", "type": "string", "enum": [ - "otel-host-bridge" + "vsock-relay" ] }, { - "description": "Guest SSH readiness probe.\n\nRetained for the SSH-compatibility window (old-generation VMs that predate guestd). New generations use [`ProcessRole::GuestControlHealth`] for framework readiness instead.", + "description": "Host-to-observability-VM OTLP bridge.", "type": "string", "enum": [ - "guest-ssh-readiness" + "otel-host-bridge" ] }, { - "description": "Authenticated guest-control Health readiness probe.\n\nReplaces [`ProcessRole::GuestSshReadiness`] as the framework readiness gate on guest-control-capable VMs: readiness is a full authenticated Hello + token challenge-response + Health over the guest-control vsock, not a raw TCP-22 probe. It fails CLOSED.", + "description": "Authenticated ComponentSession Health readiness probe.\n\nReadiness is a full authenticated ComponentSession identity exchange and Health check over the enrolled vsock, not a raw TCP-22 probe. It fails closed.", "type": "string", "enum": [ - "guest-control-health" + "component-session-health" ] }, { @@ -706,7 +666,7 @@ } }, { - "description": "Authenticated guest-control Health probe. Readiness requires a full Hello + token challenge-response + Health over the guest-control vsock - the host-side probe. Unlike [`Self::ComponentSpecific`] (which reports ready unconditionally and would fail OPEN), this predicate fails CLOSED: it is ready only when the daemon completes the authenticated probe and the guest reports a healthy/degraded state, and never ready for an old-generation / unreachable / auth-failed / timed-out guest. The daemon resolves the per-VM vsock socket, peer credentials, and broker-backed signer from its own trusted state.", + "description": "Authenticated component-session Health probe. Readiness requires a full identity-bound handshake + Health over the ComponentSession vsock - the host-side probe. Unlike [`Self::ComponentSpecific`] (which reports ready unconditionally and would fail OPEN), this predicate fails CLOSED: it is ready only when the daemon completes the authenticated probe and the guest reports a healthy/degraded state, and never ready for an old-generation / unreachable / auth-failed / timed-out guest. The daemon resolves the per-VM vsock socket, peer credentials, and enrolled identity evidence from its own trusted state.", "type": "object", "required": [ "kind", @@ -716,7 +676,7 @@ "kind": { "type": "string", "enum": [ - "guest-control-health" + "component-session-health" ] }, "value": { diff --git a/docs/reference/schemas/v2/realm-controllers.json b/docs/reference/schemas/v2/realm-controllers.json index e3aa9901d1..9abbf43d57 100644 --- a/docs/reference/schemas/v2/realm-controllers.json +++ b/docs/reference/schemas/v2/realm-controllers.json @@ -247,7 +247,6 @@ "required": [ "configSync", "exec", - "guestControl", "inGuestObservability", "keys", "shell", @@ -260,9 +259,6 @@ "exec": { "type": "boolean" }, - "guestControl": { - "type": "boolean" - }, "inGuestObservability": { "type": "boolean" }, @@ -424,13 +420,13 @@ "RealmControllerLocalWorkloadPaths": { "type": "object", "required": [ - "guestControlDir", + "componentSessionDir", "runDir", "stateDir", "storeView" ], "properties": { - "guestControlDir": { + "componentSessionDir": { "$ref": "#/definitions/PathTemplate" }, "runDir": { @@ -594,7 +590,6 @@ "configSync", "display", "exec", - "guestControl", "inGuestObservability", "keys", "lifecycle", @@ -612,9 +607,6 @@ "exec": { "type": "boolean" }, - "guestControl": { - "type": "boolean" - }, "inGuestObservability": { "type": "boolean" }, diff --git a/docs/reference/schemas/v2/storage.json b/docs/reference/schemas/v2/storage.json index b1fe53a080..88c7412dee 100644 --- a/docs/reference/schemas/v2/storage.json +++ b/docs/reference/schemas/v2/storage.json @@ -116,7 +116,6 @@ "boot", "process-exit-with-proof", "vm-stop-with-proof", - "cutover-only", "external" ] }, diff --git a/docs/reference/schemas/v2/sync.json b/docs/reference/schemas/v2/sync.json index 43f37c31e5..8b860aed92 100644 --- a/docs/reference/schemas/v2/sync.json +++ b/docs/reference/schemas/v2/sync.json @@ -257,7 +257,6 @@ "pidfd-proof-required", "cgroup-empty-proof-required", "file-record-owner-match", - "cutover-only", "manual-recovery" ] }, diff --git a/docs/reference/schemas/v2/wire-protocol.json b/docs/reference/schemas/v2/wire-protocol.json index 97cfbd6114..7116e1e51b 100644 --- a/docs/reference/schemas/v2/wire-protocol.json +++ b/docs/reference/schemas/v2/wire-protocol.json @@ -145,12 +145,54 @@ "default": false, "type": "boolean" }, + "toGeneration": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, "vm": { "type": "string" } }, "additionalProperties": false }, + "ActivationRunnerInput": { + "description": "Closed input delivered to an activation-runner `EphemeralProcess`.\n\nThe broker serializes this value into the bounded stdin envelope consumed by `d2b-activation-helper`. It deliberately carries no executable, store path, target path, or caller-controlled bytes.", + "type": "object", + "required": [ + "activationMode", + "systemArtifactId", + "targetGeneration" + ], + "properties": { + "activationMode": { + "description": "Closed activation mode.", + "allOf": [ + { + "$ref": "#/definitions/ActivationMode" + } + ] + }, + "systemArtifactId": { + "description": "Private-catalog artifact identifier.", + "allOf": [ + { + "$ref": "#/definitions/ArtifactId" + } + ] + }, + "targetGeneration": { + "description": "Target generation ordinal bound to the owning `NixosGeneration`.", + "type": "integer", + "format": "uint64", + "minimum": 1.0 + } + }, + "additionalProperties": false + }, "ApplyHostGenerationHandoff": { "description": "Typed broker request. The broker derives its target from this authenticated request and never accepts a path, command, or authority token.", "type": "object", @@ -436,21 +478,21 @@ "description": "How audio enforcement is applied for a target VM (ADR 0041).\n\nThis enum describes only *successful* enforcement outcomes. Provider misconfiguration is not a successful outcome and is reported exclusively through [`AudioVmError`] with `kind = AudioErrorKind::ProviderMisconfigured`.", "oneOf": [ { - "description": "Host-side PipeWire policy and guest-side guestd policy both applied.", + "description": "Host-side PipeWire policy and target-local Process policy both applied.", "type": "string", "enum": [ "host-and-guest" ] }, { - "description": "Host-side enforcement only; guestd is absent or the provider does not support guest enforcement.", + "description": "Host-side enforcement only; the provider does not support target-local enforcement.", "type": "string", "enum": [ "host-only" ] }, { - "description": "Guest-side guestd enforcement only; no local PipeWire node (e.g. ACA sandbox with a guestd-compatible agent).", + "description": "Target-local Process enforcement only; no local PipeWire node (for example, an ACA sandbox with a provider agent).", "type": "string", "enum": [ "guest-only" @@ -469,7 +511,7 @@ "description": "Low-cardinality error kind for a per-VM audio failure.\n\nUsed in [`AudioVmError`] and [`VmAudioErrorOutputV1`](crate::cli_output::VmAudioErrorOutputV1) so that both the wire protocol and the generated CLI schema carry enum constraints rather than a free-form string.\n\nSerde names match the canonical wire strings exactly.", "oneOf": [ { - "description": "The provider is expected to expose a guestd-compatible agent but none was found; operator remediation is required.", + "description": "The provider is expected to expose a target-local audio Process but none was found; operator remediation is required.", "type": "string", "enum": [ "provider-misconfigured" @@ -669,7 +711,7 @@ ] }, { - "description": "ACA sandbox with a guestd-compatible agent for audio policy.", + "description": "ACA sandbox with a provider agent for audio policy.", "type": "string", "enum": [ "aca-sandbox" @@ -681,14 +723,14 @@ "description": "Whether a set-volume or mute operation was applied and through which path.", "oneOf": [ { - "description": "Applied to both host (PipeWire/qemu) and guest (guestd).", + "description": "Applied to both host (PipeWire/qemu) and the target-local Process.", "type": "string", "enum": [ "host-and-guest" ] }, { - "description": "Applied to host only; guestd enforcement was unavailable or degraded.", + "description": "Applied to host only; target-local enforcement was unavailable or degraded.", "type": "string", "enum": [ "host-only" @@ -1364,63 +1406,6 @@ } } }, - { - "description": "Launch one operation-scoped cutover runner before control-plane drain.\n\nThe request must carry exactly one SCM_RIGHTS bootstrap descriptor. The broker resolves the runner executable from its trusted server configuration and never accepts a path or command over the wire.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "LaunchCutoverRunner" - ] - }, - "payload": { - "$ref": "#/definitions/LaunchCutoverRunnerRequest" - } - } - }, - { - "description": "Append one durable, operation-scoped cutover audit record.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "CutoverAudit" - ] - }, - "payload": { - "$ref": "#/definitions/CutoverAuditRequest" - } - } - }, - { - "description": "Dispatch one closed operation-scoped cutover or reset effect.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "CutoverEffect" - ] - }, - "payload": { - "$ref": "#/definitions/CutoverEffectRequest" - } - } - }, { "type": "object", "required": [ @@ -1696,24 +1681,6 @@ } } }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "GuestControlSign" - ] - }, - "payload": { - "$ref": "#/definitions/GuestControlSignRequest" - } - } - }, { "type": "object", "required": [ @@ -3018,63 +2985,6 @@ } } }, - { - "description": "Result of launching the operation-scoped cutover runner.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "LaunchCutoverRunner" - ] - }, - "payload": { - "$ref": "#/definitions/LaunchCutoverRunnerResponse" - } - } - }, - { - "description": "Durable cutover audit publication result.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "CutoverAudit" - ] - }, - "payload": { - "$ref": "#/definitions/CutoverAuditResponse" - } - } - }, - { - "description": "Typed operation-scoped effect result.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "CutoverEffect" - ] - }, - "payload": { - "$ref": "#/definitions/CutoverEffectResponse" - } - } - }, { "type": "object", "required": [ @@ -3312,24 +3222,6 @@ } } }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "GuestControlSign" - ] - }, - "payload": { - "$ref": "#/definitions/GuestControlSignResponse" - } - } - }, { "type": "object", "required": [ @@ -4422,7 +4314,7 @@ ] }, { - "description": "ACA sandbox with a guestd-compatible agent over the provider peer transport (no local socket or broker fd involved).", + "description": "ACA sandbox with a provider agent over the provider peer transport (no local socket or broker fd involved).", "type": "string", "enum": [ "aca-sandbox" @@ -4757,782 +4649,532 @@ }, "additionalProperties": false }, - "CutoverAdmissionRequest": { - "description": "Typed request for the broker-owned apply admission observation.", + "DelegateCgroupV2Request": { + "description": "The slice path is pinned by the bundle (`/sys/fs/cgroup/d2b.slice`). It is **not** taken from caller input - the broker reads it from its own bundle copy via `scope_id`.", "type": "object", + "required": [ + "scopeId" + ], "properties": { - "sourceSystemArtifactId": { - "description": "Preserved source artifact resolved and verified before host drain.", - "anyOf": [ - { - "$ref": "#/definitions/ArtifactId" - }, - { - "type": "null" - } - ] + "scopeId": { + "type": "string" }, - "systemArtifactId": { - "description": "Candidate artifact resolved and verified before host drain.", - "anyOf": [ - { - "$ref": "#/definitions/ArtifactId" - }, - { - "type": "null" - } + "tracingSpanId": { + "default": null, + "type": [ + "string", + "null" ] } }, "additionalProperties": false }, - "CutoverAdmissionResponse": { - "description": "Broker-owned apply admission observations. These fields are never caller-authored; the broker derives them from its trusted bundle and live host ownership state.", + "DeleteBridgeRequest": { + "description": "See [`CreateBridgeRequest`] for the opaque-ID rationale; `DeleteBridge` follows the same contract.", "type": "object", "required": [ - "candidateCurrent", - "markersValid", - "ownershipValid", - "predicatesHold" + "bundleBridgeIntentRef", + "scopeId" ], "properties": { - "candidateCurrent": { - "type": "boolean" - }, - "markersValid": { - "type": "boolean" + "bundleBridgeIntentRef": { + "type": "string" }, - "ownershipValid": { - "type": "boolean" + "scopeId": { + "type": "string" }, - "predicatesHold": { - "type": "boolean" + "tracingSpanId": { + "default": null, + "type": [ + "string", + "null" + ] } }, "additionalProperties": false }, - "CutoverAuditRequest": { - "description": "Request to publish one durable cutover audit transition.", + "DeletePersistentTapRequest": { + "description": "Delete one trusted attachment realization without accepting its ifname, path, or ownership marker from the caller.", "type": "object", "required": [ - "operationId", - "phase", - "requestDigest", - "transition" + "attachmentId", + "expectedAttachmentGeneration", + "expectedNetworkGeneration" ], "properties": { - "operationId": { - "description": "Operation identity bound by the runner capability.", - "type": "string" + "attachmentId": { + "$ref": "#/definitions/ResourceUid" }, - "phase": { - "description": "Current U3 phase number.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + "expectedAttachmentGeneration": { + "$ref": "#/definitions/ResourceGeneration" + }, + "expectedNetworkGeneration": { + "$ref": "#/definitions/ResourceGeneration" }, - "reasonDigest": { - "description": "Digest of a bounded hold reason, when present.", + "tracingSpanId": { + "default": null, "type": [ "string", "null" ] - }, - "requestDigest": { - "description": "Digest of the immutable operation request.", - "type": "string" - }, - "transition": { - "description": "Closed transition kind.", - "allOf": [ - { - "$ref": "#/definitions/CutoverAuditTransition" - } - ] } }, "additionalProperties": false }, - "CutoverAuditResponse": { - "description": "Durable audit publication response.", + "DeniedCommandHint": { "type": "object", "required": [ - "recordId" + "command", + "reason" ], "properties": { - "recordId": { - "description": "Stable record identity returned only after fsync and directory sync.", + "command": { + "type": "string" + }, + "reason": { "type": "string" } }, "additionalProperties": false }, - "CutoverAuditTransition": { - "description": "Closed transition vocabulary for cutover audit publication.", - "oneOf": [ - { - "description": "A hold request was durably recorded.", - "type": "string", - "enum": [ - "hold-requested" - ] - }, - { - "description": "A hold clear/resume was durably recorded.", - "type": "string", - "enum": [ - "hold-cleared" - ] - }, - { - "description": "A phase began after its journal record.", - "type": "string", - "enum": [ - "phase-started" - ] + "DeregisterRunnerPidfdRequest": { + "type": "object", + "required": [ + "roleId", + "vmId" + ], + "properties": { + "expectedStartTimeTicks": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 }, - { - "description": "A phase completed after its typed effect.", - "type": "string", - "enum": [ - "phase-completed" + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process cleanup.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } ] }, - { - "description": "A typed effect began.", - "type": "string", - "enum": [ - "effect-started" + "pid": { + "description": "Optional exact process identity. Deregistration must not remove a replacement runner that reused the VM/role tuple.", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "resourceRef": { + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } ] }, - { - "description": "A typed effect completed.", - "type": "string", - "enum": [ - "effect-completed" + "resourceUid": { + "anyOf": [ + { + "$ref": "#/definitions/ResourceUid" + }, + { + "type": "null" + } ] }, - { - "description": "A terminal outcome was recorded.", - "type": "string", - "enum": [ - "terminal" + "roleId": { + "type": "string" + }, + "tracingSpanId": { + "default": null, + "type": [ + "string", + "null" ] + }, + "vmId": { + "type": "string" } - ] + }, + "additionalProperties": false }, - "CutoverEffectAuthority": { - "description": "Closed authority carried by a cutover effect request.", - "oneOf": [ - { - "description": "Host-wide cutover authority.", - "type": "string", - "enum": [ - "cutover" + "DeregisterRunnerPidfdResponse": { + "type": "object", + "required": [ + "removed", + "roleId", + "vmId" + ], + "properties": { + "removed": { + "type": "boolean" + }, + "roleId": { + "type": "string" + }, + "vmId": { + "type": "string" + } + }, + "additionalProperties": false + }, + "DiskInitRequest": { + "description": "Disk-image provisioning request.\n\nThe daemon sends the VM's opaque `vm_id`; the broker resolves every `DiskInit` plan-op from the trusted bundle's `ProcessNode.plan_ops` for that VM and creates or validates the disk images before runner spawn. Existing `ifAbsent` images are skipped only after fd-bound identity and ext4-superblock validation; declared owner/mode posture drift is repaired automatically when the held fd is safe, and a present unformatted image is repaired only when it is proven empty. Otherwise the broker fails closed.\n\nSecurity: the broker NEVER trusts a caller-supplied path. All `target_path`, `size_bytes`, `mode`, `owner_uid`, and `owner_gid` values come from the bundle; the caller supplies only an opaque VM identifier.", + "type": "object", + "required": [ + "vmId" + ], + "properties": { + "tracingSpanId": { + "default": null, + "type": [ + "string", + "null" ] }, + "vmId": { + "type": "string" + } + }, + "additionalProperties": false + }, + "DisplayEnvironmentPosture": { + "description": "Graphical display routing applied to a workload.", + "oneOf": [ { - "description": "Zone-scoped reset authority.", + "description": "Display routing is owned by the runtime/provider.", "type": "string", "enum": [ - "reset-zone" + "runtime-managed" ] }, { - "description": "Provider-scoped reset authority.", + "description": "`DISPLAY` is removed and `WAYLAND_DISPLAY` points at the d2b proxy.", "type": "string", "enum": [ - "reset-provider" + "wayland-proxy-only" ] }, { - "description": "Guest-scoped reset authority.", + "description": "The workload has no graphical display route.", "type": "string", "enum": [ - "reset-guest" + "not-applicable" ] } ] }, - "CutoverEffectKind": { - "description": "Closed effect vocabulary shared with the U3 allowlist.", - "type": "string", - "enum": [ - "host-drain", - "cutover-disposition", - "resource-store-create", - "provider-install", - "zone-activation", - "guest-activation", - "verification", - "cutover-finalization", - "scoped-zone-reset", - "scoped-provider-reset", - "scoped-guest-reset", - "destroy-durable-volume", - "preserve-source", - "quarantine-destination", - "cutover-broker", - "closure-activation", - "apply-admission" - ] - }, - "CutoverEffectOutcome": { - "description": "Typed effect result.", + "EnvironmentClass": { + "description": "Semantic environment inheritance class.", "type": "string", "enum": [ - "succeeded", - "failed", - "ambiguous" + "minimal", + "safe-inherited", + "provider-defined" ] }, - "CutoverEffectPayload": { - "description": "Typed payloads for cutover effects that reuse existing broker operations.\n\nEvery variant is itself a closed broker request. It carries no host path, command, uid/gid, or free-form mutation text.", + "EnvironmentPosture": { + "description": "Environment source used for workload execution.", "oneOf": [ { - "type": "object", - "required": [ - "kind" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "None" - ] - } - } + "description": "Environment is owned by the runtime/provider.", + "type": "string", + "enum": [ + "runtime-managed" + ] }, { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "ApplyAdmission" - ] - }, - "payload": { - "$ref": "#/definitions/CutoverAdmissionRequest" - } - } + "description": "Environment is copied from the systemd user manager.", + "type": "string", + "enum": [ + "systemd-user-manager-ambient" + ] + } + ] + }, + "Error": { + "type": "object", + "required": [ + "code", + "docsAnchor", + "kind", + "message", + "owningCommand", + "remediation" + ], + "properties": { + "code": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Storage" - ] - }, - "payload": { - "$ref": "#/definitions/ReconcileStorageScopeRequest" - } - } + "docsAnchor": { + "type": "string" }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "ZoneStore" - ] - }, - "payload": { - "$ref": "#/definitions/OpenZoneStoreRequest" - } - } + "kind": { + "$ref": "#/definitions/Kind" }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "StoreSync" - ] - }, - "payload": { - "$ref": "#/definitions/StoreSyncRequest" - } - } + "message": { + "type": "string" }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "StoreVerify" - ] - }, - "payload": { - "$ref": "#/definitions/StoreVerifyRequest2" - } - } - }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Verification" - ] - }, - "payload": { - "$ref": "#/definitions/CutoverVerificationRequest" - } - } - }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Activation" - ] - }, - "payload": { - "$ref": "#/definitions/RunActivationRequest" - } - } - }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Systemd" - ] - }, - "payload": { - "$ref": "#/definitions/SystemdUnitRequest" - } - } - }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Quarantine" - ] - }, - "payload": { - "type": "object", - "required": [ - "marker_digest", - "source_id", - "staged_id" - ], - "properties": { - "marker_digest": { - "type": "string" - }, - "source_id": { - "type": "string" - }, - "staged_id": { - "type": "string" - } - } - } - } - }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "Finalization" - ] - }, - "payload": { - "type": "object", - "required": [ - "artifacts", - "consent_digest", - "disposition_digest" - ], - "properties": { - "artifacts": { - "type": "array", - "items": { - "$ref": "#/definitions/ArtifactId" - } - }, - "consent_digest": { - "type": "string" - }, - "disposition_digest": { - "type": "string" - } - } - } - } + "owningCommand": { + "type": "string" }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "DestroyDurableVolume" - ] - }, - "payload": { - "type": "object", - "required": [ - "consent_digest", - "marker_digest", - "storage_ref" - ], - "properties": { - "consent_digest": { - "type": "string" - }, - "marker_digest": { - "type": "string" - }, - "storage_ref": { - "type": "string" - } - } - } - } + "remediation": { + "type": "string" } - ] + } }, - "CutoverEffectRequest": { - "description": "Typed operation-scoped effect request.", + "ExecCloseArgs": { + "description": "`Close` op args. Half-closes the session's stdin (the daemon issues a guest stdin close, NOT a session teardown): the command keeps running and its output keeps flowing until it exits or the owner disconnects. The result reports `stdinClosed`. Idempotent: closing stdin on a session whose stdin is already closed/torn-down returns success.", "type": "object", "required": [ - "authority", - "capabilityDigest", - "effect", - "effectId", - "operationId", - "phase", - "replayClass", - "requestDigest" + "session" ], "properties": { - "authority": { - "$ref": "#/definitions/CutoverEffectAuthority" - }, - "capabilityDigest": { - "type": "string" - }, - "effect": { - "$ref": "#/definitions/CutoverEffectKind" - }, - "effectId": { - "type": "string" - }, - "handoff": { - "description": "Existing typed generation handoff used only by `ClosureActivation`.", - "anyOf": [ - { - "$ref": "#/definitions/ApplyHostGenerationHandoff" - }, - { - "type": "null" - } - ] - }, - "identity": { - "type": [ - "string", - "null" - ] - }, - "operationId": { - "type": "string" - }, - "payload": { - "anyOf": [ - { - "$ref": "#/definitions/CutoverEffectPayload" - }, - { - "type": "null" - } - ] - }, - "phase": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "replayClass": { - "$ref": "#/definitions/CutoverReplayClass" - }, - "requestDigest": { + "session": { "type": "string" } }, "additionalProperties": false }, - "CutoverEffectResponse": { - "description": "Typed operation-scoped effect response.", + "ExecCloseResult": { + "description": "`Close` op result. `stdinClosed` confirms the session's stdin is now half-closed; the command continues running until it exits.", "type": "object", - "required": [ - "auditRecordId", - "outcome" - ], "properties": { - "admission": { - "anyOf": [ - { - "$ref": "#/definitions/CutoverAdmissionResponse" - }, - { - "type": "null" - } - ] - }, - "auditRecordId": { - "type": "string" - }, - "identity": { - "type": [ - "string", - "null" - ] - }, - "outcome": { - "$ref": "#/definitions/CutoverEffectOutcome" - }, - "verification": { - "anyOf": [ - { - "$ref": "#/definitions/CutoverVerificationResponse" - }, - { - "type": "null" - } - ] + "stdinClosed": { + "default": false, + "type": "boolean" } }, "additionalProperties": false }, - "CutoverReplayClass": { - "description": "Closed replay behavior for an operation-scoped effect.", - "type": "string", - "enum": [ - "repeatable", - "reopen-by-journaled-identity", - "quarantine-only" - ] - }, - "CutoverVerificationRequest": { - "description": "Broker-owned phase-9 verification admission. The supplied Zone identities are expected evidence bindings only; the broker returns the live observations from its trusted bundle and host checks.", + "ExecControlResult": { + "description": "`Signal` / `Resize` op result.", "type": "object", - "required": [ - "expectedZoneIds" - ], "properties": { - "expectedZoneIds": { - "type": "array", - "items": { - "type": "string" - } + "delivered": { + "default": false, + "type": "boolean" } }, "additionalProperties": false }, - "CutoverVerificationResponse": { - "description": "Broker-owned phase-9 verification observations.", + "ExecDetachedCreateResult": { + "description": "Detached `Start` op result: an opaque exec id plus initial detached state.", "type": "object", "required": [ - "candidateCurrent", - "identityDigestsMatch", - "sourcesPreserved", - "zones" + "execId", + "state" ], "properties": { - "candidateCurrent": { - "type": "boolean" - }, - "identityDigestsMatch": { - "type": "boolean" - }, - "sourcesPreserved": { - "type": "boolean" + "execId": { + "type": "string" }, - "zones": { - "type": "array", - "items": { - "$ref": "#/definitions/CutoverZoneVerification" - } + "state": { + "$ref": "#/definitions/ExecState" } }, "additionalProperties": false }, - "CutoverZoneVerification": { - "description": "One broker-owned Zone verification observation.", + "ExecDetachedKillArgs": { + "description": "`Kill` op args for one detached exec.", "type": "object", "required": [ - "healthy", - "zoneId" + "execId", + "vm" ], "properties": { - "healthy": { - "type": "boolean" + "execId": { + "type": "string" }, - "zoneId": { + "vm": { "type": "string" } }, "additionalProperties": false }, - "DelegateCgroupV2Request": { - "description": "The slice path is pinned by the bundle (`/sys/fs/cgroup/d2b.slice`). It is **not** taken from caller input - the broker reads it from its own bundle copy via `scope_id`.", - "type": "object", - "required": [ - "scopeId" + "ExecDetachedKillOutcome": { + "description": "Closed result for idempotent detached `Kill`.", + "type": "string", + "enum": [ + "cancelling", + "already-terminal" + ] + }, + "ExecDetachedKillResult": { + "description": "Detached `Kill` op result.", + "type": "object", + "required": [ + "execId", + "result", + "state" ], "properties": { - "scopeId": { + "execId": { "type": "string" }, - "tracingSpanId": { - "default": null, - "type": [ - "string", - "null" - ] + "result": { + "$ref": "#/definitions/ExecDetachedKillOutcome" + }, + "state": { + "$ref": "#/definitions/ExecState" } }, "additionalProperties": false }, - "DeleteBridgeRequest": { - "description": "See [`CreateBridgeRequest`] for the opaque-ID rationale; `DeleteBridge` follows the same contract.", + "ExecDetachedListArgs": { + "description": "`List` op args for detached execs in one VM.", "type": "object", "required": [ - "bundleBridgeIntentRef", - "scopeId" + "vm" ], "properties": { - "bundleBridgeIntentRef": { - "type": "string" - }, - "scopeId": { + "vm": { "type": "string" - }, - "tracingSpanId": { - "default": null, - "type": [ - "string", - "null" - ] } }, "additionalProperties": false }, - "DeletePersistentTapRequest": { - "description": "Delete one trusted attachment realization without accepting its ifname, path, or ownership marker from the caller.", + "ExecDetachedListEntry": { + "description": "One detached exec summary for `List`.", "type": "object", "required": [ - "attachmentId", - "expectedAttachmentGeneration", - "expectedNetworkGeneration" + "droppedBytes", + "execId", + "startedAt", + "state" ], "properties": { - "attachmentId": { - "$ref": "#/definitions/ResourceUid" + "droppedBytes": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "expectedAttachmentGeneration": { - "$ref": "#/definitions/ResourceGeneration" + "endOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "expectedNetworkGeneration": { - "$ref": "#/definitions/ResourceGeneration" + "execId": { + "type": "string" }, - "tracingSpanId": { - "default": null, + "exitCode": { "type": [ - "string", + "integer", "null" - ] + ], + "format": "int32" + }, + "signal": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "startOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "startedAt": { + "type": "string" + }, + "state": { + "$ref": "#/definitions/ExecState" + }, + "stderrDroppedBytes": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stderrEndOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stderrStartOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stderrTruncated": { + "default": false, + "type": "boolean" + }, + "stdoutDroppedBytes": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stdoutEndOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stdoutStartOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stdoutTruncated": { + "default": false, + "type": "boolean" + }, + "truncated": { + "default": false, + "type": "boolean" } }, "additionalProperties": false }, - "DeniedCommandHint": { + "ExecDetachedListResult": { + "description": "Detached `List` op result.", "type": "object", "required": [ - "command", - "reason" + "execs" ], "properties": { - "command": { - "type": "string" - }, - "reason": { - "type": "string" + "execs": { + "type": "array", + "items": { + "$ref": "#/definitions/ExecDetachedListEntry" + } } }, "additionalProperties": false }, - "DeregisterRunnerPidfdRequest": { + "ExecDetachedLogsArgs": { + "description": "`Logs` op args for one detached exec. The daemon fetches retained stdout and stderr bytes, optionally resuming each stream from a caller-provided byte cursor, and returns them in one redacted-debug response.", "type": "object", "required": [ - "roleId", - "vmId" + "execId", + "vm" ], "properties": { - "expectedStartTimeTicks": { + "execId": { + "type": "string" + }, + "maxLen": { "type": [ "integer", "null" @@ -5540,231 +5182,137 @@ "format": "uint64", "minimum": 0.0 }, - "pid": { - "description": "Optional exact process identity. Deregistration must not remove a replacement runner that reused the VM/role tuple.", + "stderrOffset": { "type": [ "integer", "null" ], - "format": "int32" - }, - "resourceRef": { - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "resourceUid": { - "anyOf": [ - { - "$ref": "#/definitions/ResourceUid" - }, - { - "type": "null" - } - ] - }, - "roleId": { - "type": "string" + "format": "uint64", + "minimum": 0.0 }, - "tracingSpanId": { - "default": null, + "stdoutOffset": { "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint64", + "minimum": 0.0 }, - "vmId": { + "vm": { "type": "string" } }, "additionalProperties": false }, - "DeregisterRunnerPidfdResponse": { + "ExecDetachedLogsResult": { + "description": "Detached `Logs` op result. The base64 payloads carry raw guest output and are redacted from `Debug`.", "type": "object", "required": [ - "removed", - "roleId", - "vmId" + "droppedBytes", + "endOffset", + "execId", + "startOffset", + "stderrBase64", + "stdoutBase64" ], "properties": { - "removed": { - "type": "boolean" + "droppedBytes": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "roleId": { + "endOffset": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "execId": { "type": "string" }, - "vmId": { + "startOffset": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stderrBase64": { "type": "string" - } - }, - "additionalProperties": false - }, - "DiskInitRequest": { - "description": "Disk-image provisioning request.\n\nThe daemon sends the VM's opaque `vm_id`; the broker resolves every `DiskInit` plan-op from the trusted bundle's `ProcessNode.plan_ops` for that VM and creates or validates the disk images before runner spawn. Existing `ifAbsent` images are skipped only after fd-bound identity and ext4-superblock validation; declared owner/mode posture drift is repaired automatically when the held fd is safe, and a present unformatted image is repaired only when it is proven empty. Otherwise the broker fails closed.\n\nSecurity: the broker NEVER trusts a caller-supplied path. All `target_path`, `size_bytes`, `mode`, `owner_uid`, and `owner_gid` values come from the bundle; the caller supplies only an opaque VM identifier.", - "type": "object", - "required": [ - "vmId" - ], - "properties": { - "tracingSpanId": { - "default": null, - "type": [ - "string", - "null" - ] }, - "vmId": { - "type": "string" - } - }, - "additionalProperties": false - }, - "DisplayEnvironmentPosture": { - "description": "Graphical display routing applied to a workload.", - "oneOf": [ - { - "description": "Display routing is owned by the runtime/provider.", - "type": "string", - "enum": [ - "runtime-managed" - ] + "stderrDroppedBytes": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - { - "description": "`DISPLAY` is removed and `WAYLAND_DISPLAY` points at the d2b proxy.", - "type": "string", - "enum": [ - "wayland-proxy-only" - ] + "stderrEndOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - { - "description": "The workload has no graphical display route.", - "type": "string", - "enum": [ - "not-applicable" - ] - } - ] - }, - "EnvironmentClass": { - "description": "Semantic environment inheritance class.", - "type": "string", - "enum": [ - "minimal", - "safe-inherited", - "provider-defined" - ] - }, - "EnvironmentPosture": { - "description": "Environment source used for workload execution.", - "oneOf": [ - { - "description": "Environment is owned by the runtime/provider.", - "type": "string", - "enum": [ - "runtime-managed" - ] + "stderrEof": { + "default": false, + "type": "boolean" }, - { - "description": "Environment is copied from the systemd user manager.", - "type": "string", - "enum": [ - "systemd-user-manager-ambient" - ] - } - ] - }, - "Error": { - "type": "object", - "required": [ - "code", - "docsAnchor", - "kind", - "message", - "owningCommand", - "remediation" - ], - "properties": { - "code": { + "stderrNextOffset": { + "default": 0, "type": "integer", - "format": "uint8", + "format": "uint64", "minimum": 0.0 }, - "docsAnchor": { - "type": "string" + "stderrStartOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "kind": { - "$ref": "#/definitions/Kind" + "stderrTruncated": { + "default": false, + "type": "boolean" }, - "message": { + "stdoutBase64": { "type": "string" }, - "owningCommand": { - "type": "string" + "stdoutDroppedBytes": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "remediation": { - "type": "string" - } - } - }, - "ExecCloseArgs": { - "description": "`Close` op args. Half-closes the session's stdin (the daemon issues a guest stdin close, NOT a session teardown): the command keeps running and its output keeps flowing until it exits or the owner disconnects. The result reports `stdinClosed`. Idempotent: closing stdin on a session whose stdin is already closed/torn-down returns success.", - "type": "object", - "required": [ - "session" - ], - "properties": { - "session": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ExecCloseResult": { - "description": "`Close` op result. `stdinClosed` confirms the session's stdin is now half-closed; the command continues running until it exits.", - "type": "object", - "properties": { - "stdinClosed": { + "stdoutEndOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stdoutEof": { "default": false, "type": "boolean" - } - }, - "additionalProperties": false - }, - "ExecControlResult": { - "description": "`Signal` / `Resize` op result.", - "type": "object", - "properties": { - "delivered": { + }, + "stdoutNextOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stdoutStartOffset": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "stdoutTruncated": { "default": false, "type": "boolean" - } - }, - "additionalProperties": false - }, - "ExecDetachedCreateResult": { - "description": "Detached `Start` op result: an opaque exec id plus initial detached state.", - "type": "object", - "required": [ - "execId", - "state" - ], - "properties": { - "execId": { - "type": "string" }, - "state": { - "$ref": "#/definitions/ExecState" + "truncated": { + "default": false, + "type": "boolean" } }, "additionalProperties": false }, - "ExecDetachedKillArgs": { - "description": "`Kill` op args for one detached exec.", + "ExecDetachedStatusArgs": { + "description": "`Status` op args for one detached exec.", "type": "object", "required": [ "execId", @@ -5780,55 +5328,14 @@ }, "additionalProperties": false }, - "ExecDetachedKillOutcome": { - "description": "Closed result for idempotent detached `Kill`.", - "type": "string", - "enum": [ - "cancelling", - "already-terminal" - ] - }, - "ExecDetachedKillResult": { - "description": "Detached `Kill` op result.", - "type": "object", - "required": [ - "execId", - "result", - "state" - ], - "properties": { - "execId": { - "type": "string" - }, - "result": { - "$ref": "#/definitions/ExecDetachedKillOutcome" - }, - "state": { - "$ref": "#/definitions/ExecState" - } - }, - "additionalProperties": false - }, - "ExecDetachedListArgs": { - "description": "`List` op args for detached execs in one VM.", - "type": "object", - "required": [ - "vm" - ], - "properties": { - "vm": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ExecDetachedListEntry": { - "description": "One detached exec summary for `List`.", + "ExecDetachedStatusResult": { + "description": "Detached `Status` op result.", "type": "object", "required": [ "droppedBytes", + "endOffset", "execId", - "startedAt", + "startOffset", "state" ], "properties": { @@ -5838,7 +5345,6 @@ "minimum": 0.0 }, "endOffset": { - "default": 0, "type": "integer", "format": "uint64", "minimum": 0.0 @@ -5853,6 +5359,12 @@ ], "format": "int32" }, + "reason": { + "type": [ + "string", + "null" + ] + }, "signal": { "type": [ "integer", @@ -5862,61 +5374,13 @@ "minimum": 0.0 }, "startOffset": { - "default": 0, "type": "integer", "format": "uint64", "minimum": 0.0 }, - "startedAt": { - "type": "string" - }, "state": { "$ref": "#/definitions/ExecState" }, - "stderrDroppedBytes": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrEndOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrStartOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrTruncated": { - "default": false, - "type": "boolean" - }, - "stdoutDroppedBytes": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutEndOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutStartOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutTruncated": { - "default": false, - "type": "boolean" - }, "truncated": { "default": false, "type": "boolean" @@ -5924,267 +5388,152 @@ }, "additionalProperties": false }, - "ExecDetachedListResult": { - "description": "Detached `List` op result.", - "type": "object", - "required": [ - "execs" - ], - "properties": { - "execs": { - "type": "array", - "items": { - "$ref": "#/definitions/ExecDetachedListEntry" - } - } - }, - "additionalProperties": false - }, - "ExecDetachedLogsArgs": { - "description": "`Logs` op args for one detached exec. The daemon fetches retained stdout and stderr bytes, optionally resuming each stream from a caller-provided byte cursor, and returns them in one redacted-debug response.", + "ExecEnvVar": { + "description": "A single environment variable for `ExecOp::Start`. Values are forwarded verbatim into the guest exec request and are NEVER logged, traced, or audited (only the count is observable). `Debug` is redacted so a stray `{:?}` can never leak a key or secret value.", "type": "object", "required": [ - "execId", - "vm" + "key", + "value" ], "properties": { - "execId": { + "key": { "type": "string" }, - "maxLen": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "stderrOffset": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "stdoutOffset": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "vm": { + "value": { "type": "string" } }, "additionalProperties": false }, - "ExecDetachedLogsResult": { - "description": "Detached `Logs` op result. The base64 payloads carry raw guest output and are redacted from `Debug`.", - "type": "object", - "required": [ - "droppedBytes", - "endOffset", - "execId", - "startOffset", - "stderrBase64", - "stdoutBase64" - ], - "properties": { - "droppedBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "ExecOp": { + "description": "Multiplexed exec operation. Closed adjacently-tagged enum (`op` + `args`); each variant's args struct is `deny_unknown_fields`.", + "oneOf": [ + { + "type": "object", + "required": [ + "args", + "op" + ], + "properties": { + "args": { + "$ref": "#/definitions/ExecStartArgs" + }, + "op": { + "type": "string", + "enum": [ + "start" + ] + } + } }, - "endOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + { + "type": "object", + "required": [ + "args", + "op" + ], + "properties": { + "args": { + "$ref": "#/definitions/ExecWriteStdinArgs" + }, + "op": { + "type": "string", + "enum": [ + "writeStdin" + ] + } + } }, - "execId": { - "type": "string" + { + "type": "object", + "required": [ + "args", + "op" + ], + "properties": { + "args": { + "$ref": "#/definitions/ExecReadOutputArgs" + }, + "op": { + "type": "string", + "enum": [ + "readOutput" + ] + } + } }, - "startOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + { + "type": "object", + "required": [ + "args", + "op" + ], + "properties": { + "args": { + "$ref": "#/definitions/ExecSignalArgs" + }, + "op": { + "type": "string", + "enum": [ + "signal" + ] + } + } }, - "stderrBase64": { - "type": "string" + { + "type": "object", + "required": [ + "args", + "op" + ], + "properties": { + "args": { + "$ref": "#/definitions/ExecResizeArgs" + }, + "op": { + "type": "string", + "enum": [ + "resize" + ] + } + } }, - "stderrDroppedBytes": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 + { + "type": "object", + "required": [ + "args", + "op" + ], + "properties": { + "args": { + "$ref": "#/definitions/ExecWaitArgs" + }, + "op": { + "type": "string", + "enum": [ + "wait" + ] + } + } }, - "stderrEndOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 + { + "type": "object", + "required": [ + "args", + "op" + ], + "properties": { + "args": { + "$ref": "#/definitions/ExecCloseArgs" + }, + "op": { + "type": "string", + "enum": [ + "close" + ] + } + } }, - "stderrEof": { - "default": false, - "type": "boolean" - }, - "stderrNextOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrStartOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stderrTruncated": { - "default": false, - "type": "boolean" - }, - "stdoutBase64": { - "type": "string" - }, - "stdoutDroppedBytes": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutEndOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutEof": { - "default": false, - "type": "boolean" - }, - "stdoutNextOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutStartOffset": { - "default": 0, - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "stdoutTruncated": { - "default": false, - "type": "boolean" - }, - "truncated": { - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ExecDetachedStatusArgs": { - "description": "`Status` op args for one detached exec.", - "type": "object", - "required": [ - "execId", - "vm" - ], - "properties": { - "execId": { - "type": "string" - }, - "vm": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ExecDetachedStatusResult": { - "description": "Detached `Status` op result.", - "type": "object", - "required": [ - "droppedBytes", - "endOffset", - "execId", - "startOffset", - "state" - ], - "properties": { - "droppedBytes": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "endOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "execId": { - "type": "string" - }, - "exitCode": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "reason": { - "type": [ - "string", - "null" - ] - }, - "signal": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "startOffset": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "state": { - "$ref": "#/definitions/ExecState" - }, - "truncated": { - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ExecEnvVar": { - "description": "A single environment variable for `ExecOp::Start`. Values are forwarded verbatim into the guest exec request and are NEVER logged, traced, or audited (only the count is observable). `Debug` is redacted so a stray `{:?}` can never leak a key or secret value.", - "type": "object", - "required": [ - "key", - "value" - ], - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ExecOp": { - "description": "Multiplexed exec operation. Closed adjacently-tagged enum (`op` + `args`); each variant's args struct is `deny_unknown_fields`.", - "oneOf": [ { "type": "object", "required": [ @@ -6193,12 +5542,12 @@ ], "properties": { "args": { - "$ref": "#/definitions/ExecStartArgs" + "$ref": "#/definitions/ExecDetachedListArgs" }, "op": { "type": "string", "enum": [ - "start" + "list" ] } } @@ -6211,12 +5560,12 @@ ], "properties": { "args": { - "$ref": "#/definitions/ExecWriteStdinArgs" + "$ref": "#/definitions/ExecDetachedLogsArgs" }, "op": { "type": "string", "enum": [ - "writeStdin" + "logs" ] } } @@ -6229,12 +5578,12 @@ ], "properties": { "args": { - "$ref": "#/definitions/ExecReadOutputArgs" + "$ref": "#/definitions/ExecDetachedStatusArgs" }, "op": { "type": "string", "enum": [ - "readOutput" + "status" ] } } @@ -6247,252 +5596,126 @@ ], "properties": { "args": { - "$ref": "#/definitions/ExecSignalArgs" + "$ref": "#/definitions/ExecDetachedKillArgs" }, "op": { "type": "string", "enum": [ - "signal" + "kill" ] } } - }, + } + ] + }, + "ExecOpResponse": { + "description": "Multiplexed exec operation result. Closed adjacently-tagged enum mirroring [`ExecOp`].", + "oneOf": [ { "type": "object", "required": [ - "args", - "op" + "op", + "result" ], "properties": { - "args": { - "$ref": "#/definitions/ExecResizeArgs" - }, "op": { "type": "string", "enum": [ - "resize" + "start" ] + }, + "result": { + "$ref": "#/definitions/ExecStartResult" } } }, { "type": "object", "required": [ - "args", - "op" + "op", + "result" ], "properties": { - "args": { - "$ref": "#/definitions/ExecWaitArgs" - }, "op": { "type": "string", "enum": [ - "wait" + "detachedCreate" ] + }, + "result": { + "$ref": "#/definitions/ExecDetachedCreateResult" } } }, { "type": "object", "required": [ - "args", - "op" + "op", + "result" ], "properties": { - "args": { - "$ref": "#/definitions/ExecCloseArgs" - }, "op": { "type": "string", "enum": [ - "close" + "writeStdin" ] + }, + "result": { + "$ref": "#/definitions/ExecWriteStdinResult" } } }, { "type": "object", "required": [ - "args", - "op" + "op", + "result" ], "properties": { - "args": { - "$ref": "#/definitions/ExecDetachedListArgs" - }, "op": { "type": "string", "enum": [ - "list" + "readOutput" ] + }, + "result": { + "$ref": "#/definitions/ExecReadOutputResult" } } }, { "type": "object", "required": [ - "args", - "op" + "op", + "result" ], "properties": { - "args": { - "$ref": "#/definitions/ExecDetachedLogsArgs" - }, "op": { "type": "string", "enum": [ - "logs" + "signal" ] + }, + "result": { + "$ref": "#/definitions/ExecControlResult" } } }, { "type": "object", "required": [ - "args", - "op" + "op", + "result" ], "properties": { - "args": { - "$ref": "#/definitions/ExecDetachedStatusArgs" - }, "op": { "type": "string", "enum": [ - "status" + "resize" ] - } - } - }, - { - "type": "object", - "required": [ - "args", - "op" - ], - "properties": { - "args": { - "$ref": "#/definitions/ExecDetachedKillArgs" - }, - "op": { - "type": "string", - "enum": [ - "kill" - ] - } - } - } - ] - }, - "ExecOpResponse": { - "description": "Multiplexed exec operation result. Closed adjacently-tagged enum mirroring [`ExecOp`].", - "oneOf": [ - { - "type": "object", - "required": [ - "op", - "result" - ], - "properties": { - "op": { - "type": "string", - "enum": [ - "start" - ] - }, - "result": { - "$ref": "#/definitions/ExecStartResult" - } - } - }, - { - "type": "object", - "required": [ - "op", - "result" - ], - "properties": { - "op": { - "type": "string", - "enum": [ - "detachedCreate" - ] - }, - "result": { - "$ref": "#/definitions/ExecDetachedCreateResult" - } - } - }, - { - "type": "object", - "required": [ - "op", - "result" - ], - "properties": { - "op": { - "type": "string", - "enum": [ - "writeStdin" - ] - }, - "result": { - "$ref": "#/definitions/ExecWriteStdinResult" - } - } - }, - { - "type": "object", - "required": [ - "op", - "result" - ], - "properties": { - "op": { - "type": "string", - "enum": [ - "readOutput" - ] - }, - "result": { - "$ref": "#/definitions/ExecReadOutputResult" - } - } - }, - { - "type": "object", - "required": [ - "op", - "result" - ], - "properties": { - "op": { - "type": "string", - "enum": [ - "signal" - ] - }, - "result": { - "$ref": "#/definitions/ExecControlResult" - } - } - }, - { - "type": "object", - "required": [ - "op", - "result" - ], - "properties": { - "op": { - "type": "string", - "enum": [ - "resize" - ] - }, - "result": { - "$ref": "#/definitions/ExecControlResult" + }, + "result": { + "$ref": "#/definitions/ExecControlResult" } } }, @@ -6821,6 +6044,7 @@ "additionalProperties": false }, "ExecState": { + "description": "Lifecycle state projected for a target-local Process or EphemeralProcess resource.", "type": "string", "enum": [ "created", @@ -6830,7 +6054,7 @@ "cancelled", "slow-consumer-cancelled", "protocol-error", - "lost-guestd", + "lost-target", "reaped" ] }, @@ -7469,928 +6693,488 @@ "result": { "$ref": "#/definitions/GatewayDisplayListDetailedResult" } - } - } - ] - }, - "GatewayDisplayOpenArgs": { - "type": "object", - "required": [ - "appArgv", - "operationId", - "principal", - "requestHash", - "target" - ], - "properties": { - "appArgv": { - "type": "array", - "items": { - "type": "string" - } - }, - "operationId": { - "type": "string" - }, - "principal": { - "type": "string" - }, - "requestHash": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "target": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GatewayDisplayOpenResult": { - "type": "object", - "required": [ - "sessionId", - "state" - ], - "properties": { - "sessionId": { - "type": "string" - }, - "state": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GatewayDisplaySessionDetail": { - "type": "object", - "required": [ - "operationId", - "principal", - "sessionId", - "state", - "target" - ], - "properties": { - "operationId": { - "type": "string" - }, - "principal": { - "type": "string" - }, - "sessionId": { - "type": "string" - }, - "state": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GatewayDisplaySessionSummary": { - "type": "object", - "required": [ - "sessionId", - "state", - "target" - ], - "properties": { - "sessionId": { - "type": "string" - }, - "state": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GatewayDisplayStartArgs": { - "type": "object", - "required": [ - "operationId", - "principal", - "requestHash", - "target" - ], - "properties": { - "operationId": { - "type": "string" - }, - "principal": { - "type": "string" - }, - "requestHash": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "target": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GatewayDisplayStartResult": { - "type": "object", - "required": [ - "state", - "target" - ], - "properties": { - "state": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GatewayDisplayStopArgs": { - "type": "object", - "required": [ - "operationId", - "principal", - "requestHash", - "target" - ], - "properties": { - "operationId": { - "type": "string" - }, - "principal": { - "type": "string" - }, - "requestHash": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "target": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GatewayDisplayStopResult": { - "type": "object", - "required": [ - "state", - "target" - ], - "properties": { - "state": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GcRequest": { - "description": "Common flags every mutating-verb request carries. The daemon rejects requests that set neither `dry_run` nor `apply`.", - "type": "object", - "properties": { - "apply": { - "default": false, - "type": "boolean" - }, - "dryRun": { - "default": false, - "type": "boolean" - }, - "json": { - "default": false, - "type": "boolean" - }, - "keepGenerations": { - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "GraphicalLaunchPosture": { - "description": "Closed graphical launch posture.", - "type": "string", - "enum": [ - "proxied", - "not-applicable", - "graphical-session-inactive", - "wayland-unavailable", - "proxy-unavailable" - ] - }, - "GuestBootIdWire": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "GuestControlAuthPurpose": { - "type": "string", - "enum": [ - "guest-control-auth-v1" - ] - }, - "GuestControlDirection": { - "type": "string", - "enum": [ - "host-to-guest" - ] - }, - "GuestControlProofRole": { - "type": "string", - "enum": [ - "host-proof", - "guest-proof" - ] - }, - "GuestControlSignRequest": { - "type": "object", - "required": [ - "direction", - "guestBootId", - "guestControlPort", - "guestNonce", - "hostNonce", - "protocolVersion", - "purpose", - "role", - "vmId" - ], - "properties": { - "capabilitiesHash": { - "type": [ - "string", - "null" - ] - }, - "direction": { - "$ref": "#/definitions/GuestControlDirection" - }, - "guestBootId": { - "$ref": "#/definitions/GuestBootIdWire" - }, - "guestControlPort": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "guestNonce": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 - }, - "hostNonce": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 - }, - "peerCid": { - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "protocolVersion": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "purpose": { - "$ref": "#/definitions/GuestControlAuthPurpose" - }, - "role": { - "$ref": "#/definitions/GuestControlProofRole" - }, - "tracingSpanId": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "vmId": { - "type": "string" - } - }, - "additionalProperties": false - }, - "GuestControlSignResponse": { - "type": "object", - "required": [ - "tag" - ], - "properties": { - "tag": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "maxItems": 32, - "minItems": 32 - } - }, - "additionalProperties": false - }, - "HandoffCallerRole": { - "description": "Caller-derived broker role for the handoff operation.", - "oneOf": [ - { - "description": "Lifecycle-authorized operator.", - "type": "string", - "enum": [ - "lifecycle" - ] - }, - { - "description": "Administrator.", - "type": "string", - "enum": [ - "admin" - ] - } - ] - }, - "HandoffState": { - "description": "Durable handoff phase.", - "oneOf": [ - { - "description": "Intent was durably recorded.", - "type": "string", - "enum": [ - "recorded" - ] - }, - { - "description": "Target closure and ancestry passed validation.", - "type": "string", - "enum": [ - "validated" - ] - }, - { - "description": "Target mutation is in progress.", - "type": "string", - "enum": [ - "mutating" - ] - }, - { - "description": "Ownership transferred to the target broker.", - "type": "string", - "enum": [ - "transferred" - ] - }, - { - "description": "Target completed and source may be retired.", - "type": "string", - "enum": [ - "completed" - ] - }, - { - "description": "Source was retained after refusal or rollback.", - "type": "string", - "enum": [ - "rolled-back" - ] - }, - { - "description": "Request was refused before mutation.", - "type": "string", - "enum": [ - "refused" - ] + } } ] }, - "Hello": { + "GatewayDisplayOpenArgs": { "type": "object", "required": [ - "clientVersion" + "appArgv", + "operationId", + "principal", + "requestHash", + "target" ], "properties": { - "clientVersion": { - "$ref": "#/definitions/SemverRange" - }, - "supportedFeatures": { - "default": [], + "appArgv": { "type": "array", "items": { - "$ref": "#/definitions/FeatureFlag" + "type": "string" } + }, + "operationId": { + "type": "string" + }, + "principal": { + "type": "string" + }, + "requestHash": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "target": { + "type": "string" } }, "additionalProperties": false }, - "HelloOk": { + "GatewayDisplayOpenResult": { "type": "object", "required": [ - "capabilities", - "selectedVersion", - "serverVersion" + "sessionId", + "state" ], "properties": { - "capabilities": { - "type": "array", - "items": { - "$ref": "#/definitions/FeatureFlag" - } - }, - "selectedVersion": { - "$ref": "#/definitions/Version" + "sessionId": { + "type": "string" }, - "serverVersion": { - "$ref": "#/definitions/Version" + "state": { + "type": "string" } }, "additionalProperties": false }, - "HelloRejected": { + "GatewayDisplaySessionDetail": { "type": "object", "required": [ - "reason" + "operationId", + "principal", + "sessionId", + "state", + "target" ], "properties": { - "reason": { - "$ref": "#/definitions/HelloRejectedReason" + "operationId": { + "type": "string" + }, + "principal": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "state": { + "type": "string" + }, + "target": { + "type": "string" } }, "additionalProperties": false }, - "HelloRejectedReason": { - "type": "string", - "enum": [ - "versionMismatch", - "capabilityNegotiationFailed", - "internalError" - ] - }, - "HelloRequest": { - "description": "Daemon ↔ broker handshake request. Carries the daemon's client_version and the wire feature flags it understands so the broker can pick a compatible response version + capability set. Mirrors the bootstrap `Hello { client_version, supported_features }` shape.", + "GatewayDisplaySessionSummary": { "type": "object", "required": [ - "clientVersion" + "sessionId", + "state", + "target" ], "properties": { - "clientVersion": { + "sessionId": { "type": "string" }, - "supportedFeatures": { - "default": [], - "type": "array", - "items": { - "type": "string" - } + "state": { + "type": "string" + }, + "target": { + "type": "string" } }, "additionalProperties": false }, - "HelloResponse": { - "description": "Daemon ↔ broker handshake response. Mirrors the bootstrap `BrokerResponse::HelloOk` shape so the connection-level capability negotiation works without a side-channel.", + "GatewayDisplayStartArgs": { "type": "object", "required": [ - "capabilities", - "selectedVersion", - "serverVersion" + "operationId", + "principal", + "requestHash", + "target" ], "properties": { - "capabilities": { - "type": "array", - "items": { - "type": "string" - } + "operationId": { + "type": "string" }, - "selectedVersion": { + "principal": { "type": "string" }, - "serverVersion": { + "requestHash": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "target": { "type": "string" } }, "additionalProperties": false }, - "HostCheckRequest": { + "GatewayDisplayStartResult": { "type": "object", + "required": [ + "state", + "target" + ], "properties": { - "readOnly": { - "default": true, - "type": "boolean" + "state": { + "type": "string" }, - "strict": { - "default": false, - "type": "boolean" + "target": { + "type": "string" } }, "additionalProperties": false }, - "HostCheckResponse": { + "GatewayDisplayStopArgs": { "type": "object", "required": [ - "exitCode", - "findings" + "operationId", + "principal", + "requestHash", + "target" ], "properties": { - "exitCode": { + "operationId": { + "type": "string" + }, + "principal": { + "type": "string" + }, + "requestHash": { "type": "integer", - "format": "uint8", + "format": "uint64", "minimum": 0.0 }, - "findings": { - "type": "array", - "items": { - "$ref": "#/definitions/HostFinding" - } + "target": { + "type": "string" } }, "additionalProperties": false }, - "HostCutoverInventorySummary": { - "description": "Redaction-safe inventory summary for a host-wide preview.", + "GatewayDisplayStopResult": { "type": "object", "required": [ - "complete", - "inventoryDigest", - "itemCount", - "zoneCount" + "state", + "target" ], "properties": { - "complete": { - "description": "Whether every configured Zone was observed.", - "type": "boolean" + "state": { + "type": "string" }, - "inventoryDigest": { - "description": "Canonical inventory digest.", + "target": { "type": "string" + } + }, + "additionalProperties": false + }, + "GcRequest": { + "description": "Common flags every mutating-verb request carries. The daemon rejects requests that set neither `dry_run` nor `apply`.", + "type": "object", + "properties": { + "apply": { + "default": false, + "type": "boolean" }, - "itemCount": { - "description": "Number of classified inventory items.", - "type": "integer", + "dryRun": { + "default": false, + "type": "boolean" + }, + "json": { + "default": false, + "type": "boolean" + }, + "keepGenerations": { + "default": null, + "type": [ + "integer", + "null" + ], "format": "uint32", "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "GraphicalLaunchPosture": { + "description": "Closed graphical launch posture.", + "type": "string", + "enum": [ + "proxied", + "not-applicable", + "graphical-session-inactive", + "wayland-unavailable", + "proxy-unavailable" + ] + }, + "GuestExecutionBinding": { + "description": "Exact authenticated binding for a Guest-local Process lifecycle.\n\nThe values are commitments, not raw boot identifiers or transport handles. A Guest broker requires this tuple for every target-local Process operation and revalidates the boot commitment against its own kernel before executing the request.", + "type": "object", + "required": [ + "assignmentEpoch", + "bootIdentityDigest", + "controllerGeneration", + "providerGeneration", + "sessionGeneration", + "targetUid" + ], + "properties": { + "assignmentEpoch": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "bootIdentityDigest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32 }, - "zoneCount": { - "description": "Number of configured Zones included.", + "controllerGeneration": { "type": "integer", - "format": "uint32", + "format": "uint64", + "minimum": 0.0 + }, + "providerGeneration": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "sessionGeneration": { + "type": "integer", + "format": "uint64", "minimum": 0.0 + }, + "targetUid": { + "$ref": "#/definitions/ResourceUid" } }, "additionalProperties": false }, - "HostCutoverOperation": { - "description": "Closed host cutover command vocabulary.", + "HandoffCallerRole": { + "description": "Caller-derived broker role for the handoff operation.", "oneOf": [ { - "description": "Build a mutation-free preview.", - "type": "string", - "enum": [ - "preview" - ] - }, - { - "description": "Read the current redacted status.", + "description": "Lifecycle-authorized operator.", "type": "string", "enum": [ - "status" + "lifecycle" ] }, { - "description": "Set an operator hold.", + "description": "Administrator.", "type": "string", "enum": [ - "hold" + "admin" ] - }, + } + ] + }, + "HandoffState": { + "description": "Durable handoff phase.", + "oneOf": [ { - "description": "Clear a hold and resume.", + "description": "Intent was durably recorded.", "type": "string", "enum": [ - "resume" + "recorded" ] }, { - "description": "Consume apply consent and launch the runner.", + "description": "Target closure and ancestry passed validation.", "type": "string", "enum": [ - "apply" + "validated" ] }, { - "description": "Roll back before the native boundary.", + "description": "Target mutation is in progress.", "type": "string", "enum": [ - "rollback" + "mutating" ] }, { - "description": "Verify every configured Zone.", + "description": "Ownership transferred to the target broker.", "type": "string", "enum": [ - "verify" + "transferred" ] }, { - "description": "Read-only cutover health diagnostics.", + "description": "Target completed and source may be retired.", "type": "string", "enum": [ - "doctor" + "completed" ] }, { - "description": "Consume phase-10 consent.", + "description": "Source was retained after refusal or rollback.", "type": "string", "enum": [ - "finalize" + "rolled-back" ] }, { - "description": "Run a distinct scoped reset.", + "description": "Request was refused before mutation.", "type": "string", "enum": [ - "reset" + "refused" ] } ] }, - "HostCutoverRequest": { - "description": "One host cutover or scoped-reset command.", + "Hello": { "type": "object", "required": [ - "operation" + "clientVersion" ], "properties": { - "candidateId": { - "description": "Candidate identity bound to the request.", - "type": [ - "string", - "null" - ] - }, - "consentDigest": { - "description": "Exact apply or finalization consent digest.", - "type": [ - "string", - "null" - ] - }, - "consentJson": { - "description": "Canonical serialized U3 apply consent.", - "type": [ - "string", - "null" - ] - }, - "destroyDurableVolumes": { - "description": "Whether this reset explicitly authorizes durable-Volume destruction.", - "type": [ - "boolean", - "null" - ] - }, - "destructiveConsentDigest": { - "description": "Separate destructive reset consent digest.", - "type": [ - "string", - "null" - ] - }, - "destructiveConsentJson": { - "description": "Canonical destructive reset consent artifact.", - "type": [ - "string", - "null" - ] - }, - "freshConsentDigest": { - "description": "Fresh consent used by a non-owner Admin to resume.", - "type": [ - "string", - "null" - ] - }, - "hostDigest": { - "description": "Host identity digest bound by the recovery attestation.", - "type": [ - "string", - "null" - ] - }, - "operation": { - "description": "Requested operation.", - "allOf": [ - { - "$ref": "#/definitions/HostCutoverOperation" - } - ] - }, - "operationId": { - "description": "Existing operation identity for status or continuation.", - "type": [ - "string", - "null" - ] - }, - "operatorId": { - "description": "Bound operator identity.", - "type": [ - "string", - "null" - ] - }, - "previewDigest": { - "description": "Exact preview digest.", - "type": [ - "string", - "null" - ] - }, - "reason": { - "description": "Bounded hold reason.", - "type": [ - "string", - "null" - ] - }, - "recoveryAttestationJson": { - "description": "Canonical serialized qualified recovery attestation.", - "type": [ - "string", - "null" - ] - }, - "recoveryDigest": { - "description": "Qualified recovery digest for cutover apply.", - "type": [ - "string", - "null" - ] - }, - "resetScope": { - "description": "Scoped reset authority, required only for `reset`.", - "anyOf": [ - { - "$ref": "#/definitions/HostCutoverResetScope" - }, - { - "type": "null" - } - ] - }, - "revisionPlanId": { - "description": "Revision-plan identity bound to the request.", - "type": [ - "string", - "null" - ] - }, - "sourceSystemArtifactId": { - "description": "Preserved source artifact identity bound to native rollback.", - "type": [ - "string", - "null" - ] - }, - "systemArtifactId": { - "description": "System artifact identity bound to the frozen cutover preview and handoff.", - "type": [ - "string", - "null" - ] - }, - "target": { - "description": "Opaque target identity for a scoped reset.", - "type": [ - "string", - "null" - ] + "clientVersion": { + "$ref": "#/definitions/SemverRange" }, - "zone": { - "description": "Zone selection is forbidden for one-time cutover operations.", - "type": [ - "string", - "null" - ] + "supportedFeatures": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/FeatureFlag" + } } }, "additionalProperties": false }, - "HostCutoverResetScope": { - "description": "Closed scoped-reset authority vocabulary.", - "oneOf": [ - { - "description": "Reset one Zone.", - "type": "string", - "enum": [ - "zone" - ] + "HelloOk": { + "type": "object", + "required": [ + "capabilities", + "selectedVersion", + "serverVersion" + ], + "properties": { + "capabilities": { + "type": "array", + "items": { + "$ref": "#/definitions/FeatureFlag" + } }, - { - "description": "Reset one Provider.", - "type": "string", - "enum": [ - "provider" - ] + "selectedVersion": { + "$ref": "#/definitions/Version" }, - { - "description": "Reset one Guest.", - "type": "string", - "enum": [ - "guest" - ] + "serverVersion": { + "$ref": "#/definitions/Version" + } + }, + "additionalProperties": false + }, + "HelloRejected": { + "type": "object", + "required": [ + "reason" + ], + "properties": { + "reason": { + "$ref": "#/definitions/HelloRejectedReason" } + }, + "additionalProperties": false + }, + "HelloRejectedReason": { + "type": "string", + "enum": [ + "versionMismatch", + "capabilityNegotiationFailed", + "internalError" ] }, - "HostCutoverResponse": { - "description": "Redaction-safe host cutover response.", + "HelloRequest": { + "description": "Daemon ↔ broker handshake request. Carries the daemon's client_version and the wire feature flags it understands so the broker can pick a compatible response version + capability set. Mirrors the bootstrap `Hello { client_version, supported_features }` shape.", "type": "object", "required": [ - "mutationAccepted", - "operation", - "phase", - "state", - "summary" + "clientVersion" ], "properties": { - "inventory": { - "description": "Inventory summary without paths, secrets, or host identifiers.", - "anyOf": [ - { - "$ref": "#/definitions/HostCutoverInventorySummary" - }, - { - "type": "null" - } - ] + "clientVersion": { + "type": "string" }, - "mutationAccepted": { - "description": "Whether this response represents an accepted mutation.", - "type": "boolean" + "supportedFeatures": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "HelloResponse": { + "description": "Daemon ↔ broker handshake response. Mirrors the bootstrap `BrokerResponse::HelloOk` shape so the connection-level capability negotiation works without a side-channel.", + "type": "object", + "required": [ + "capabilities", + "selectedVersion", + "serverVersion" + ], + "properties": { + "capabilities": { + "type": "array", + "items": { + "type": "string" + } }, - "operation": { - "description": "Command that produced this response.", - "allOf": [ - { - "$ref": "#/definitions/HostCutoverOperation" - } - ] + "selectedVersion": { + "type": "string" }, - "operationId": { - "description": "Opaque operation identity, when one exists.", - "type": [ - "string", - "null" - ] + "serverVersion": { + "type": "string" + } + }, + "additionalProperties": false + }, + "HostCheckRequest": { + "type": "object", + "properties": { + "readOnly": { + "default": true, + "type": "boolean" }, - "phase": { - "description": "Current protocol phase number.", + "strict": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "HostCheckResponse": { + "type": "object", + "required": [ + "exitCode", + "findings" + ], + "properties": { + "exitCode": { "type": "integer", "format": "uint8", "minimum": 0.0 }, - "previewDigest": { - "description": "Preview digest, when the operation has a preview.", - "type": [ - "string", - "null" - ] - }, - "state": { - "description": "Redacted U3 operation state.", - "type": "string" - }, - "summary": { - "description": "Redaction-safe summary.", - "type": "string" + "findings": { + "type": "array", + "items": { + "$ref": "#/definitions/HostFinding" + } } }, "additionalProperties": false @@ -8779,7 +7563,7 @@ ] }, { - "description": "A provider required by an audio or console operation is present but not in a state where enforcement can proceed (e.g. expected guestd agent absent). Operator remediation required.", + "description": "A provider required by an audio or console operation is present but not in a state where enforcement can proceed (e.g. expected target-local Process absent). Operator remediation required.", "type": "string", "enum": [ "provider-misconfigured" @@ -8787,75 +7571,6 @@ } ] }, - "LaunchCutoverRunnerRequest": { - "description": "Request to launch the one-shot cutover runner.", - "type": "object", - "required": [ - "bootstrapFdIndex", - "capabilityDigest", - "expiresAtMs", - "operationId" - ], - "properties": { - "bootstrapFdIndex": { - "description": "Required index of the single bootstrap fd attachment.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "capabilityDigest": { - "description": "Digest of the capability transferred over the bootstrap fd. The broker binds this value to the operation before spawning the runner.", - "type": "string" - }, - "expiresAtMs": { - "description": "Capability expiry copied from the single-use bootstrap.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "operationId": { - "description": "Opaque operation identity used to derive runner-owned state.", - "type": "string" - } - }, - "additionalProperties": false - }, - "LaunchCutoverRunnerResponse": { - "description": "Response from the cutover runner launch.", - "type": "object", - "required": [ - "operationId", - "pid", - "startTimeTicks" - ], - "properties": { - "operationId": { - "description": "Opaque operation identity.", - "type": "string" - }, - "pid": { - "description": "Child pid observed at launch.", - "type": "integer", - "format": "int32" - }, - "pidfdIndex": { - "description": "Index of the returned pidfd, when the broker supplies one.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "startTimeTicks": { - "description": "`/proc//stat` start-time captured by the broker.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "LaunchMinijailChildRequest": { "description": "The daemon never passes argv, env, uid/gid, caps, seccomp profile path, or any other launch authority across the wire. The broker reads the full launch context from `bundle.vms[vm_id].roles[role_id]` and constructs the minijail exec line itself.", "type": "object", @@ -9394,6 +8109,17 @@ "bundleRunnerIntentRef": { "type": "string" }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process adoption.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "resourceRef": { "description": "Optional generic Process identity binding.", "anyOf": [ @@ -9685,6 +8411,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process adoption.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "pid": { "description": "PID the snapshot recorded.", "type": "integer", @@ -9823,6 +8560,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process operations.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Process Provider identity digest.", "type": "array", @@ -10806,45 +9554,7 @@ } }, { - "description": "Host-wide cutover and scoped-reset operation family.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "host cutover" - ] - }, - "payload": { - "$ref": "#/definitions/HostCutoverRequest" - } - } - }, - { - "description": "Read the editable guest config working copy of `vm` over the authenticated guest-control bridge and return it as a base64 string. ADMIN-ONLY (it crosses into the guest over the authenticated transport): the daemon enforces `PeerRole::Admin` BEFORE any probe / sign / read. The CLI's `config sync` uses this on guest-control VMs instead of an SSH transfer.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "read guest config" - ] - }, - "payload": { - "$ref": "#/definitions/ReadGuestConfigRequest" - } - } - }, - { - "description": "Multiplexed, ADMIN-ONLY operation on a daemon-held authenticated guest-control exec session. A single owner connection issues a `Start` op then drives the session with the remaining ops (`WriteStdin`/`ReadOutput`/`Signal`/`Resize`/`Wait`/`Close`). The daemon enforces `PeerRole::Admin` BEFORE any session lookup, vsock connect, auth, or `ExecCreate`. `d2b vm exec` drives this verb; it never crosses SSH.", + "description": "Multiplexed, ADMIN-ONLY operation on a daemon-held authenticated Process resource session. A single owner connection issues a `Start` op then drives the session with named-stream operations.", "type": "object", "required": [ "kind", @@ -11099,25 +9809,6 @@ } } }, - { - "description": "Host-wide cutover or scoped-reset response.", - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "host cutover" - ] - }, - "payload": { - "$ref": "#/definitions/HostCutoverResponse" - } - } - }, { "type": "object", "required": [ @@ -11208,24 +9899,6 @@ } } }, - { - "type": "object", - "required": [ - "kind", - "payload" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "read guest config" - ] - }, - "payload": { - "$ref": "#/definitions/ReadGuestConfigResponse" - } - } - }, { "type": "object", "required": [ @@ -11856,32 +10529,6 @@ "connection-lost-during-shutdown" ] }, - "ReadGuestConfigRequest": { - "description": "`read guest config` request payload. The daemon resolves the per-VM vsock socket + peer credentials from the trusted bundle; the client supplies only the VM name.", - "type": "object", - "required": [ - "vm" - ], - "properties": { - "vm": { - "type": "string" - } - }, - "additionalProperties": false - }, - "ReadGuestConfigResponse": { - "description": "`read guest config` response payload. `contentBase64` is the standard padded base64 of the RAW guest config bytes. The encoded payload is bounded by `READ_GUEST_CONFIG_ENCODED_MAX_BYTES` (derived from `READ_GUEST_FILE_MAX_BYTES`) so it always fits within the public.sock and ttRPC frames. The CLI decodes it and computes size + sha256 from the DECODED bytes - never from any guest-reported value.", - "type": "object", - "required": [ - "contentBase64" - ], - "properties": { - "contentBase64": { - "type": "string" - } - }, - "additionalProperties": false - }, "RealmId": { "type": "string", "maxLength": 128, @@ -12467,6 +11114,13 @@ "qemu-media" ] }, + { + "description": "Target-local one-shot NixOS activation runner. Guest mode may spawn this role only from the bundle-authoritative process intent.", + "type": "string", + "enum": [ + "activation-nixos" + ] + }, { "description": "virtiofsd sidecar; one per `microvm.shares` row. The daemon/bundle provides argv from `nixos-modules/processes-json.nix`.", "type": "string", @@ -12579,7 +11233,6 @@ "required": [ "configSync", "exec", - "guestControl", "inGuestObservability", "keys", "ssh" @@ -12591,9 +11244,6 @@ "exec": { "type": "boolean" }, - "guestControl": { - "type": "boolean" - }, "inGuestObservability": { "type": "boolean" }, @@ -12701,7 +11351,7 @@ "audio", "video", "network", - "guest-control", + "component-session", "usb", "observability" ] @@ -13387,6 +12037,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process control.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "pid": { "type": [ "integer", @@ -13533,6 +12194,17 @@ "vmId" ], "properties": { + "activationInput": { + "description": "Typed stdin input admitted only for the activation-nixos runner role.", + "anyOf": [ + { + "$ref": "#/definitions/ActivationRunnerInput" + }, + { + "type": "null" + } + ] + }, "bundleContentIdentity": { "description": "Content identity of the daemon's trusted bundle snapshot.", "type": [ @@ -13576,6 +12248,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process launches.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Provider/template identity expected by the daemon.", "type": [ @@ -13747,6 +12430,16 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "pid": { "description": "Child PID. The daemon validates this against the pidfd it received and against `/proc//stat` field 22 (`start_time`).", "type": "integer", @@ -13967,6 +12660,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process operations.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Process Provider identity digest.", "type": "array", @@ -14366,6 +13070,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding, when this is Guest-local.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "invocationId": { "description": "systemd's 16-byte InvocationID.", "type": "array", @@ -14463,6 +13178,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process operations.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Process Provider identity digest.", "type": "array", diff --git a/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json b/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json index 69d9bc9230..d5cb67c5f2 100644 --- a/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json +++ b/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json @@ -3,6 +3,78 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "definitions": { + "ActivationMode": { + "description": "Target-local activation mode.", + "oneOf": [ + { + "description": "Activate immediately for the running target.", + "enum": [ + "switch" + ], + "type": "string" + }, + { + "description": "Select the generation for the next boot.", + "enum": [ + "boot" + ], + "type": "string" + }, + { + "description": "Activate temporarily until the next boot.", + "enum": [ + "test" + ], + "type": "string" + }, + { + "description": "Record an already active generation without running a helper.", + "enum": [ + "adopt" + ], + "type": "string" + } + ] + }, + "ActivationRunnerInput": { + "additionalProperties": false, + "description": "Closed input delivered to an activation-runner `EphemeralProcess`.\n\nThe broker serializes this value into the bounded stdin envelope consumed by `d2b-activation-helper`. It deliberately carries no executable, store path, target path, or caller-controlled bytes.", + "properties": { + "activationMode": { + "allOf": [ + { + "$ref": "#/definitions/ActivationMode" + } + ], + "description": "Closed activation mode." + }, + "systemArtifactId": { + "allOf": [ + { + "$ref": "#/definitions/ArtifactId" + } + ], + "description": "Private-catalog artifact identifier." + }, + "targetGeneration": { + "description": "Target generation ordinal bound to the owning `NixosGeneration`.", + "format": "uint64", + "minimum": 1.0, + "type": "integer" + } + }, + "required": [ + "activationMode", + "systemArtifactId", + "targetGeneration" + ], + "type": "object" + }, + "ArtifactId": { + "maxLength": 63, + "minLength": 1, + "type": "string" + }, "BoundedToken": { "maxLength": 63, "minLength": 1, @@ -544,6 +616,16 @@ "additionalProperties": false, "description": "The EphemeralProcess ResourceType base spec.\n\nEphemeralProcess is the one-shot process itself, never a job that references a Process, and it carries no restart, readiness, health check, or adoption field.", "properties": { + "activationInput": { + "anyOf": [ + { + "$ref": "#/definitions/ActivationRunnerInput" + }, + { + "type": "null" + } + ] + }, "budget": { "$ref": "#/definitions/BudgetSpec" }, diff --git a/docs/reference/schemas/v3/core.d2bus.org_Volume.schema.json b/docs/reference/schemas/v3/core.d2bus.org_Volume.schema.json index fec0c0ba95..a0b6e20f58 100644 --- a/docs/reference/schemas/v3/core.d2bus.org_Volume.schema.json +++ b/docs/reference/schemas/v3/core.d2bus.org_Volume.schema.json @@ -123,7 +123,6 @@ "boot", "process-exit-with-proof", "vm-stop-with-proof", - "cutover-only", "external" ], "type": "string" diff --git a/docs/reference/security-manual-validation-checklist.md b/docs/reference/security-manual-validation-checklist.md deleted file mode 100644 index 3932f86402..0000000000 --- a/docs/reference/security-manual-validation-checklist.md +++ /dev/null @@ -1,104 +0,0 @@ -# d2b v3 security manual validation checklist - -This checklist is the human evidence companion for the destructive U6 -cutover lane. It does not replace the production U3 engine, U4 broker -boundary, U5 delivery validators, or the booted-VM rehearsal. - -## Candidate and recovery binding - -- [ ] The candidate is committed, clean, frozen, and its commit, tree, bundle - generation, closure digest, and protected GC root are recorded. -- [ ] The preview was produced by `d2b host cutover preview` without `--zone`. -- [ ] The preview is byte-stable on repeat and `inventory.complete=true`. -- [ ] `inventory.zoneCount` equals every configured Zone on the host. -- [ ] The candidate snapshot, seal, recovery attestation, consent, handoff, and - verification files are the exact intended files. -- [ ] `recovery-import`, `seal`, and `merge-eligibility` passed on that - candidate; no validator result was copied from another candidate. -- [ ] The external recovery point is full-host, same-host, read-only through - expiry, and covers boot/system state, every previewed artifact, preserved - identity state, and the exact restore instructions. -- [ ] The external provider's readback or backup verification passed. -- [ ] The attestation has enough remaining lifetime for apply, verify, guarded - merge, and post-merge seal. -- [ ] The local checkpoint is treated as evidence only, never as the R19 - recovery point. - -## Consent and admission - -- [ ] Apply consent is canonical, single-use, and bound to the exact - operation, candidate, preview, recovery digest, and operator. -- [ ] A wrong candidate, tree, closure, preview, host, operator, restore - instruction digest, or recovery locator fails before mutation. -- [ ] A stale, expired, replayed, duplicate, fractional, negative, or - unknown-field record fails closed. -- [ ] The CLI is run as the bound Admin principal; no Launcher, shutdown hook, - or unrelated Admin is substituted. -- [ ] The broker admitted one operation-scoped bootstrap capability and the - runner consumed it once. -- [ ] The runner is outside `d2b.slice`, owns the journal and OFD lock, and - no persistent cutover systemd unit was installed. - -## Drain, restart, and typed effects - -- [ ] The runner remains reachable through its owner-authenticated socket after - the client disconnects. -- [ ] Any configured Admin can set a safety hold. -- [ ] Only the bound operator can resume without fresh consent; a non-owner - resume without its digest is refused. -- [ ] The runner drains `d2bd` only after the journal records the transition. -- [ ] The adapted broker remains the only privileged effect path after drain. -- [ ] The typed host-generation handoff names an authenticated Host target, - generation ancestry, compatibility floor, and catalog artifact. It - contains no path or command. -- [ ] Each effect is in the closed cutover allowlist, has the expected phase - and replay class, and publishes durable audit evidence before success. -- [ ] A daemon restart reattaches read-only observation and never repairs or - adopts the runner journal. - -## Rollback and restore boundary - -- [ ] A phase-0 through phase-4 interruption rolls back through the preserved - source generation, quarantines staged destinations, and leaves TPM, - durable Volumes, store-view, SSH keys, and audit bytes unchanged. -- [ ] A phase-5-or-later interruption refuses native rollback and reports - `RestoreRequired`. -- [ ] The exact external recovery mechanism was exercised in the VM rehearsal - after a phase-5-or-later failure. -- [ ] The operator's restore instructions were independently verified and are - bound by digest. -- [ ] After the drill, identity and audit digests match the recovery point. -- [ ] No replacement candidate, native rollback, or finalization was attempted - after the external-restore boundary. - -## Verification and finalization - -- [ ] `d2b host cutover verify` observes exactly the configured Zone set. -- [ ] Every Zone is healthy and the candidate remains current. -- [ ] Preserved sources and identity digests match. -- [ ] Audit continuity is durable across bootstrap, hold/resume, drain, - generation handoff, effect completion, and verification. -- [ ] `d2b host cutover doctor` reports no cutover or adoption quarantine and - no unresolved ownership or recovery degradation. -- [ ] The observed stop state is `CutoverSucceeded`. -- [ ] The U6 lane did not invoke `d2b host cutover finalize`. -- [ ] U7 obtained a separate digest-bound finalization consent only after - guarded merge, post-merge reconciliation, post-merge seal, and fresh - status/verify/doctor checks. -- [ ] The finalization disposition contains only approved operation-owned - artifacts; the active closure, recovery material, journal, store-view - roots, and audit segments remain protected. - -## Redaction evidence - -- [ ] Logs contain fixed labels, typed failure classes, phase numbers, and - approved digests only. -- [ ] No raw host path, store path, socket, hostname, PID, UID, username, - recovery locator, restore text, credential, token, SSH key, or realm - principal appears in logs, audit fields, CLI errors, or support output. -- [ ] Recovery evidence stores only the canonical attestation digest and - opaque locator digest; the recovery payload remains with the external - provider. -- [ ] Any screenshot or terminal capture was redacted before retention. -- [ ] Rehearsal examples use generic identities and RFC1918/RFC5737 ranges, - never daily-driver host data. diff --git a/docs/reference/security-runbook.md b/docs/reference/security-runbook.md index 4abe0e0f74..0dddba7bff 100644 --- a/docs/reference/security-runbook.md +++ b/docs/reference/security-runbook.md @@ -38,7 +38,7 @@ view is usually this simpler table: | Actor | Socket / entry point | What they can do | | --- | --- | --- | | `launcherUsers` | `/run/d2b/public.sock` via the CLI | Day-to-day public CLI traffic: read-only verbs plus mutating verbs that dispatch through d2bd → broker (v1.0 daemon-only per ADR 0015). Treat membership as privileged host access. | -| `adminUsers` | `/run/d2b/public.sock` via the CLI | Everything launchers can do, plus the admin-only surfaces: the `audit` export, and the destructive guest-control exec verb (`vm exec`) that runs arbitrary commands as the VM's workload user (`ssh.user`, never root) in a PAM login session over the authenticated transport (operators elevate with `sudo` inside the session). The daemon enforces the admin requirement with `SO_PEERCRED` at `public.sock` accept time, before any session work, and denies non-admin callers with an `authz-not-admin` error. | +| `adminUsers` | `/run/d2b/public.sock` via the CLI | Everything launchers can do, plus the admin-only surfaces: the `audit` export, and the destructive component-session exec verb (`vm exec`) that runs arbitrary commands as the VM's workload user (`ssh.user`, never root) in a PAM login session over the authenticated transport (operators elevate with `sudo` inside the session). The daemon enforces the admin requirement with `SO_PEERCRED` at `public.sock` accept time, before any session work, and denies non-admin callers with an `authz-not-admin` error. | | `d2bd` | `/run/d2b/priv.sock` | The **only** direct broker client. The broker re-resolves paths from the trusted bundle and emits allow/deny audit rows for privileged operations. | | `root` | host OS | Break-glass access outside the d2b control plane. | diff --git a/docs/reference/store-sync.md b/docs/reference/store-sync.md index c5410550d1..58b1216bf7 100644 --- a/docs/reference/store-sync.md +++ b/docs/reference/store-sync.md @@ -221,7 +221,7 @@ positive-allow-list projection to a dedicated export file: written `0640`, `O_APPEND`, one JSON object per line, daily-rotated by UTC date. The projection is a dedicated `StoreSyncObservabilityRecord` struct -(`packages/d2b-priv-broker/src/ops/store_sync_export.rs`) built by +(`packages/d2b-broker/src/ops/store_sync_export.rs`) built by `from_audit_fields()`, which reads **only** the allow-listed fields - so no serializer ever receives the full audit struct and host-only fields cannot leak by construction (`#[serde(deny_unknown_fields)]` + an @@ -380,11 +380,11 @@ asserts `len() == 0`. ## Implementation file map -- `packages/d2b-priv-broker/src/ops/store_sync.rs` - pure +- `packages/d2b-broker/src/ops/store_sync.rs` - pure handler (`run_store_sync`) + typed `StoreSyncError`. Derives the `generation_id`, materialises via `build_store_view_cross_mount_safe`, and publishes (`state/current`, `meta/current`, live marker). -- `packages/d2b-priv-broker/src/ops/store_view_farm.rs` - +- `packages/d2b-broker/src/ops/store_view_farm.rs` - cross-mount-safe wrappers (`build_store_view_cross_mount_safe`) that retry the build/replace in a private mount namespace when `/nix/store` is a separate vfsmount. The broker execs the activation helper directly; no shell @@ -397,7 +397,7 @@ asserts `len() == 0`. the `private-store ` entrypoint that unshares the mount namespace, makes propagation private, lazily detaches `/nix/store`, and runs `build-store-view` / `replace-store-view-live` from stdin JSON. -- `packages/d2b-priv-broker/src/runtime.rs` - wire dispatch +- `packages/d2b-broker/src/runtime.rs` - wire dispatch arm (`RealBrokerRequest::StoreSync(req) => …`). - `packages/d2b-contracts-broker/src/broker_wire.rs` - typed request/ response structs + enum variants. The wire carries both the @@ -406,12 +406,12 @@ asserts `len() == 0`. only and is never the on-disk key. - `packages/d2b-contracts/src/types.rs` - `BundleClosureRef` opaque newtype. -- `packages/d2b-priv-broker/src/ops/audit_op.rs` - +- `packages/d2b-broker/src/ops/audit_op.rs` - `OperationFields::StoreSync` newtype over `StoreSyncAuditFields`. -- `packages/d2b-priv-broker/src/ops/store_sync_audit.rs` - +- `packages/d2b-broker/src/ops/store_sync_audit.rs` - the signed `StoreSyncAuditFields` terminal audit schema, its enums, invariant-enforcing constructors, and `validate()`. -- `packages/d2b-priv-broker/src/ops/store_verify.rs` - explicit +- `packages/d2b-broker/src/ops/store_verify.rs` - explicit StoreVerify broker op, top-level live-pool verifier, and host-only integrity record writer. diff --git a/docs/reference/store-virtiofs.md b/docs/reference/store-virtiofs.md index b6f433624c..c661500c8e 100644 --- a/docs/reference/store-virtiofs.md +++ b/docs/reference/store-virtiofs.md @@ -7,9 +7,8 @@ set. Historical microvm.nix runner evidence lives in ## Framework-managed shares -For a headless `corp-vm`, d2b emits these baseline shares. The -guest-control token share is present only when -`d2b.vms.corp-vm.guest.control.enable = true`. +For a headless `corp-vm`, d2b emits these baseline shares. ComponentSession +enrollment keys are not delivered through virtiofs. | Tag | Socket | Shared dir | Mode | |---------------|------------------------------------------|-------------------------------------------------------|------| @@ -17,7 +16,6 @@ guest-control token share is present only when | `d2b-meta` | `/run/d2b/vms/corp-vm/d2b-meta.sock` | `/var/lib/d2b/vms/corp-vm/store-view/meta` | RO | | `d2b-hkeys` | `/run/d2b/vms/corp-vm/d2b-hkeys.sock` | `/var/lib/d2b/vms/corp-vm/host-keys` | RW | | `d2b-ssh-host` | `/run/d2b/vms/corp-vm/d2b-ssh-host.sock` | `/var/lib/d2b/vms/corp-vm/sshd-host-keys` | RW | -| `d2b-gctl` | `/run/d2b/vms/corp-vm/guest-control/d2b-gctl.sock` | `/var/lib/d2b/guest-control-corp-vm` | RO | CH connects to each socket via the `--fs socket=,tag=` flag (see `ChArgvInput.fs_shares` in the @@ -44,8 +42,7 @@ Flag semantics: - `--socket-path` - UDS the CH runner connects to. Daemon-owned; the broker places normal share sockets under - `/run/d2b/vms//.sock`; `d2b-gctl` uses the isolated - `/run/d2b/vms//guest-control/d2b-gctl.sock` path. + `/run/d2b/vms//.sock`. - `--socket-group=` - optional UDS group ownership. It is emitted only when `microvm.virtiofsd.group` is non-null. - `--shared-dir` - host path the guest sees through the tag. @@ -62,8 +59,7 @@ Flag semantics: - `--inode-file-handles=prefer` - virtiofsd uses `name_to_handle_at` when the underlying filesystem supports it. Reduces the per-share fd budget; matches the audit shape. -- `--readonly` - `ro-store`, `d2b-meta`, and the guest-control token - share (`d2b-gctl`) are read-only. `d2b-meta` is rooted at +- `--readonly` - `ro-store` and `d2b-meta` are read-only. `d2b-meta` is rooted at `store-view/meta` and carries only guest-safe generation metadata (`current`, `store-paths`, `db.dump`, allow-listed `meta.json`); it never exposes `live/`, `state/`, `gcroots/`, or `sync.lock`. The @@ -74,10 +70,7 @@ Flag semantics: Per ADR 0021 each virtiofsd instance runs fake-root inside a broker-pre-established single-entry user namespace and has zero host capabilities. Normal VM shares map namespace UID/GID 0 to the -`d2b--runner` stable principal. The guest-control token share -(`d2b-gctl`) maps to the narrower `d2b--gctlfs` stable -principal and receives only the token directory/file ACLs plus its -dedicated runtime socket directory. +`d2b--runner` stable principal. The CH runner's `--fs socket=` line trusts the broker to have set the socket's group ownership/ACLs so Cloud Hypervisor can connect. diff --git a/docs/reference/tap-dag-contract.md b/docs/reference/tap-dag-contract.md index 1b72a62537..9e3d4d4f40 100644 --- a/docs/reference/tap-dag-contract.md +++ b/docs/reference/tap-dag-contract.md @@ -82,7 +82,7 @@ apply-nftables-rules ---> bring-up-tap-interface ---> pre-open-vhost-net-fd race where packets traverse the bridge before the firewall policy is in place. 2. `ApplyNmUnmanaged` (host-wide, not per-VM) - enforced inside - the broker handler via [`TapCreateGate`](../../packages/d2b-priv-broker/src/ops/tap.rs). + the broker handler via [`TapCreateGate`](../../packages/d2b-broker/src/ops/tap.rs). The broker REFUSES `CreateTapFd` / `CreatePersistentTap` unless the prior `ApplyNmUnmanaged` op recorded either `NmUnmanagedOutcome::Applied` or @@ -273,7 +273,7 @@ ownership marker so `ApplyNmUnmanaged` can succeed), the next - **Host-prep DAG**: [host-prep-dag.md](./host-prep-dag.md) - parent contract for the per-VM DAG this step lives in. - **Bridge port flags**: [`packages/d2b-host/src/bridge_port.rs`](../../packages/d2b-host/src/bridge_port.rs). -- **Tap broker handlers**: [`packages/d2b-priv-broker/src/ops/tap.rs`](../../packages/d2b-priv-broker/src/ops/tap.rs). +- **Tap broker handlers**: [`packages/d2b-broker/src/ops/tap.rs`](../../packages/d2b-broker/src/ops/tap.rs). - **Derived ifname emitter**: [`packages/d2b-host/src/ifname.rs`](../../packages/d2b-host/src/ifname.rs). - **Host config DTO**: [`packages/d2b-core/src/host.rs`](../../packages/d2b-core/src/host.rs) - `ChNetHandoffMode`, `IfNameMapping`, `HostChConfig`. - **Failure envelope**: [`packages/d2b-host/src/host_prep_dag.rs`](../../packages/d2b-host/src/host_prep_dag.rs) - `HostPrepStepFailed`. diff --git a/docs/reference/tracing-contract.md b/docs/reference/tracing-contract.md index 7ed9178e56..28a0773351 100644 --- a/docs/reference/tracing-contract.md +++ b/docs/reference/tracing-contract.md @@ -4,7 +4,7 @@ > artifact checks. The d2b daemon (`d2bd`) and the privileged broker -(`d2b-priv-broker`) both emit OpenTelemetry spans and structured +(`d2b-broker`) both emit OpenTelemetry spans and structured `tracing` events. Those spans flow to the native SigNoz backend through the `OtelHostBridge` broker role and the `sys-obs` collector. To keep the backend's ClickHouse cardinality budget bounded **and** to prevent @@ -92,7 +92,7 @@ than emitting the path itself. ### Broker bundle-load span (canonical reference) -Before (`packages/d2b-priv-broker/src/runtime.rs`, pre-`b6f4ac9`): +Before (`packages/d2b-broker/src/runtime.rs`, pre-`b6f4ac9`): ```rust tracing::info!( diff --git a/docs/reference/usbip-state-machine.md b/docs/reference/usbip-state-machine.md index 1b246ea311..7ec1d0250f 100644 --- a/docs/reference/usbip-state-machine.md +++ b/docs/reference/usbip-state-machine.md @@ -40,8 +40,8 @@ current tree. A future effect adapter must establish these preconditions before using it for a live operation: - a USBIP bind intent and firewall intent exist for the VM/busid; -- VM apply paths that need guest import have a running VM and authenticated - guest-control USBIP capability; +- VM apply paths that need guest import have a running VM and an authenticated + ComponentSession USBIP Process; - `d2b.site.yubikey.enable = true` and at least one enabled VM in the env opts into `usbip.yubikey = true` before host YubiKey machinery is expected; - the broker can prepare `usbip-host`, the host-session busid lock, @@ -197,7 +197,7 @@ current host boot/session. It is not preserved across host reboot because the lock is under `/run/d2b/locks/usbip`. Only an explicit USB detach may revoke backend ACLs and release the claim during a host session, and only after firewall withdrawal/targeted flow cleanup and host unbind succeed. A -dead/unreachable VM guest-detach failure stays visible as degraded cleanup but +dead/unreachable VM target-local detach failure stays visible as degraded cleanup but does not block host-side firewall withdrawal or unbind. The cleanup plan never stops or rebinds the per-env backend/proxy sidecars. If a @@ -206,12 +206,13 @@ closed before sysfs `usbip-host` unbind, keeps the session claim, and surfaces manual recovery instead of killing the shared listener. VM start treats same-host-session same-VM USBIP session claims as required until an explicit -optional-device policy exists. Runtime absence, guest-control import failure, or +optional-device policy exists. Runtime absence, target-local import failure, or per-env proxy/backend unavailability degrades the USB row and lets boot continue with a precise remediation command. A same-owner row where the host claim is held, the device is already bound to `usbip-host`, and the guest import is detached is -convergable: the daemon may refresh the firewall/proxy path and ask guestd to -import the busid again without releasing the host-session claim. +convergable: the daemon may refresh the firewall/proxy path and ask the +target-local USBIP Process to import the busid again without releasing the +host-session claim. During backend ACL grant the broker treats `/dev/bus/usb//` as a volatile device node. It may retry across transient devnum changes or brief diff --git a/docs/reference/wave-evidence-schema.md b/docs/reference/wave-evidence-schema.md index 554881e3a2..25f199cc8f 100644 --- a/docs/reference/wave-evidence-schema.md +++ b/docs/reference/wave-evidence-schema.md @@ -291,8 +291,8 @@ also provide the remaining lifetime required for apply, verification, guarded merge, and post-merge seal; the validator does not estimate those durations. The candidate delivery state is external and candidate-addressed. Binding -request, terminal failure, cutover result, merge attempt, post-merge +request, terminal failure, merge attempt, post-merge reconciliation, post-merge seal, finalization, and close records are published once through the fd-relative synchronized writer. A binding failure, expiry, -candidate or evidence drift, cutover failure, or merge/tree mismatch writes one +candidate or evidence drift, or merge/tree mismatch writes one terminal failure and cannot be replaced. diff --git a/docs/specs/ADR-046-cli-and-operations.md b/docs/specs/ADR-046-cli-and-operations.md index 86268f20fa..c11e335870 100644 --- a/docs/specs/ADR-046-cli-and-operations.md +++ b/docs/specs/ADR-046-cli-and-operations.md @@ -12,6 +12,12 @@ | Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-componentsession-and-bus` | | Supersedes | Current v3 `d2b` CLI contract (`packages/d2b/src/lib.rs` at baseline) | +> **Current-state note (U10).** Baseline rows that name the removed Guest +> daemon or guest-control transport are historical disposition records. The +> current CLI reaches Guest execution, shell, activation, config, audio, and +> USB behavior through Resource API, ComponentSession, named streams, and +> signed target-local Processes. + ## Purpose This spec defines every public-facing CLI command, argument, output format, exit @@ -79,7 +85,7 @@ the "target:" annotation. | `ProcessRole::Gpu`/`GpuRenderNode` | `d2b-core/src/processes.rs:209,211` | `Process` under `Provider/device-gpu` | | `ProcessRole::Audio` | `d2b-core/src/processes.rs:213` | `Process` under audio-pipewire Provider | | `ProcessRole::HostReconcile` | `d2b-core/src/processes.rs:196` | Realm controller bootstrap step; not a standalone CLI-visible Process | -| `ProcessRole::GuestControlHealth` | `d2b-core/src/processes.rs:235` | Guest readiness probe integrated into Guest controller reconcile loop; not a standalone Process ResourceType | +| `ProcessRole::ComponentSessionHealth` | `d2b-core/src/processes.rs` | Guest readiness probe integrated into Guest controller reconcile loop; not a standalone Process ResourceType | | `ProcessRole::GuestSshReadiness` | `d2b-core/src/processes.rs:228` | Deleted at v3 clean cutover; no compatibility retention | | `VmProcessDag` | `d2b-core/src/processes.rs` | Per-Guest/Provider `Process` resource DAG; target managed by Provider controller | | `RealmControllersJson`, `RealmControllerLocalWorkload { vm_name, identity }` | `d2b-core/src/realm_controller_config.rs` | Zone Nix-authored Guest resources; `vm_name` → `Guest/` resource name; `identity.canonical_target` → `WorkloadId` = `Guest/` resource name | @@ -2384,7 +2390,7 @@ The following table maps every current v3 CLI verb to its v3 status. | `d2b host prepare` | **Retained** | `d2b host prepare` | `cmd_host_prepare` @ lib.rs:4468 | reachable | | `d2b host destroy` | **Retained** | `d2b host destroy` | `cmd_host_destroy` @ lib.rs:4549 | reachable | | `d2b host doctor` | **Retained** | `d2b host doctor` | `cmd_host_doctor` @ lib.rs:4752 | reachable | -| `d2b host migrate-storage` | Deleted after reset | *(none; storage ADR 0034 reset)* | `cmd_host_migrate_storage` @ lib.rs:4867 | reachable | +| `d2b host migrate-storage` | Deleted at 3.0 | *(none)* | *(removed; no current implementation)* | deleted | | `d2b host install` | **Retained** | `d2b host install` | `cmd_host_install` @ lib.rs:5007 | reachable | | `d2b host reconcile` | **Retained** | `d2b host reconcile` | `cmd_host_reconcile` @ lib.rs:5070 | reachable | | `d2b host validate` | **Retained** | `d2b host validate` | `cmd_host_validate` @ lib.rs:4941 | reachable | @@ -2418,9 +2424,8 @@ below are deleted at the 3.0 clean break. The removal criterion for each is: its live v3 successor is implemented and tested, and the v2 source path (`cmd_*` function) is deleted in the same wave. -`d2b host migrate-storage` is deleted after the v3 storage reset completes; it -has no v3 successor because the layout cutover it served is a one-time v1→v2 -migration. +`d2b host migrate-storage` has no v3 successor because the one-time v1→v2 +storage migration surface was removed from the shipped CLI. `d2b clipboard picker` is already removed from the dispatch table in the v3 baseline (only a deprecation notice remains at `lib.rs:2424`). diff --git a/docs/specs/ADR-046-components-processes-and-sandbox.md b/docs/specs/ADR-046-components-processes-and-sandbox.md index 59d328f123..c0ef051b8d 100644 --- a/docs/specs/ADR-046-components-processes-and-sandbox.md +++ b/docs/specs/ADR-046-components-processes-and-sandbox.md @@ -64,6 +64,14 @@ After bootstrap: - every process has one executionRef/domain/user placement and selected Process Provider. +Signed component descriptors carry one closed instance scope for controller +roles, a canonical Host/Guest/Zone target set, and a concrete artifact digest +plus required EffectPort classes for each advertised target. Core admits the +descriptor before creating its static Process graph; services and workers +cannot claim ResourceType ownership or controller placement. The only +in-process controller exceptions are the fixed `Provider/system-core` and +`Provider/system-minijail` bootstrap components. + ## Static Provider deployment and optional component state Volumes Core ProviderDeployment reads the signed manifest/catalog entry a Provider's diff --git a/docs/specs/ADR-046-current-code-migration-map.md b/docs/specs/ADR-046-current-code-migration-map.md index ac5a6d75ec..b1a8bbe9f1 100644 --- a/docs/specs/ADR-046-current-code-migration-map.md +++ b/docs/specs/ADR-046-current-code-migration-map.md @@ -14,6 +14,12 @@ | Foundation | `fd5b0067` - docs: establish d2b 3.0 Provider control-plane foundation | | Main reuse | Permitted (copy/adapt); main is not current-state evidence | +> **Current-state note (U10).** This map is a baseline disposition record, not +> a runtime inventory. The legacy Guest daemon, feature-specific guest-control +> protocol, token-share path, and production bridges listed below are deleted +> on the U10 head. Current execution is owned by `d2bd guest`, ComponentSession, +> signed Provider Processes, and the Guest-profile broker. + --- ## §0 Purpose and Notation @@ -71,7 +77,7 @@ Target columns use **ADR 0046 names**. This table is the authoritative translati | `WorkloadProviderKind` enum | **`UnsafeLocal` variant → `Host` ResourceType** (not `Guest.spec.providerRef`); `LocalVm`/`QemuMedia`/`ProviderManaged` variants → `Guest.spec.providerRef` pointing to respective Provider resource | D042/D050; UnsafeLocal selects Host not Guest | | `WorkloadExecutionPosture` | `ExecutionPolicy` fields shared by Host and Guest | `d2b-realm-core/src/workload.rs:83` | | Provider traits/adapters/factories | Zone-local `Provider` resource + controller/service/worker Processes | | -| guest-control ttrpc/vsock | current evidence of guestd vsock protocol; target is ComponentSession/d2b-bus | D001 - ComponentSession copies from main `a1cc0b2d` | +| guest-control ttrpc/vsock | deleted legacy protocol; current owner is ComponentSession/d2b-bus | D001 - retained only as baseline disposition evidence | | `d2b-realm-router` session types | REPLACE target; current v3 session impl to be removed | Do NOT copy from v3 | | `AllocatorEngine` / `HostResourceKind` leases | Zone bootstrap resource provisioning (Network/CgroupSubtree) | Currently schema-only | @@ -522,8 +528,8 @@ They are the replacement target, not a reuse source. ComponentSession copies fro | Symbol / Binary | Evidence | Current Definition / Callers | Disposition | Target | Work Item | |----------------|----------|------------------------------|-------------|--------|-----------| -| `d2b-guestd` - PAM login, workload user exec, `ExecOp` handler | production-reachable | Guest-control ttrpc/vsock server; callers: d2bd exec_session via guest-control | REPLACE | ComponentSession `Exec` service inside Guest; vsock transport → ComponentSession bootstrap | `ADR046-session-001` | -| `d2b-guestd` - detached runner / unit lifecycle | production-reachable | Detached exec jobs under transient user scope | ADAPT | `EphemeralProcess` worker inside Guest; keep PAM login semantics | `ADR046-primitives-002` | +| `d2b-guestd` - PAM login, workload user exec, `ExecOp` handler | deleted | Legacy Guest-control ttrpc/vsock server | DELETE | ComponentSession `Exec` service and signed Process resources | `ADR046-session-001` | +| `d2b-guestd` - detached runner / unit lifecycle | deleted | Legacy detached exec jobs under transient user scope | DELETE | `EphemeralProcess` worker inside Guest | `ADR046-primitives-002` | | `d2b-userd` - `main.rs` exits 78 for service mode; `lib.rs`: `UserExecSession` trait (attach/resize), `UserSocketPolicy`/`UserdTransport`, `UserdConfig`, `UserSessionIdentity`, `UserAttachRequest`, `UserOutputCursor`, `UserdError`; `USERD_LISTENS_ON_VSOCK = false` | production-reachable | Main exits 78 (`"service mode is not implemented"`); lib defines user exec/session traits consumed by d2bd attach path; callers: d2bd user attach handler | REPLACE | Fixed user supervisor Process (`Provider/system-systemd` user domain) + `Provider/system-systemd` user Process effects; remove public `d2b userd` verb and stub after parity | `ADR046-primitives-003` | ### 1.11 `d2b-clipd` @@ -567,7 +573,7 @@ catalog; `RunnerRole` is the supervisor-side enum). | `NetRunner` | production-reachable | Per-env tap/bridge/nft setup runner | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | | `StoreSync` | production-reachable | Per-VM `/nix/store` hardlink farm sync; broker `store_view_farm` op | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | | `ActivationHelper` | production-reachable | Per-host activation; broker `HostPrepare` | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | -| `GuestControlForwarder` | production-reachable | Guest-control vsock bridge | REPLACE | ComponentSession guest-control bootstrap replaces this role; old vsock protocol inert at d2b 3.0 cutover, no compatibility window | `ADR046-session-001` | +| `GuestControlForwarder` | deleted | Legacy Guest-control vsock bridge | DELETE | ComponentSession bootstrap and target-local Process resources | `ADR046-session-001` | | `RealmController` | production-reachable | Per-realm controller process; d2bd spawns via broker | `Process` (Zone runtime controller) | Zone runtime bootstrap | | `RealmBroker` | production-reachable | Per-realm broker process; socket-activated PID1 unit at `b5ddbed6` → REPLACE with parent-spawned Zone process per ADR 0046 | `Process` (Zone broker) | Zone runtime bootstrap | | `KeyRotation` | production-reachable | SSH key rotation; broker `KeyRotation` op | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | @@ -904,7 +910,7 @@ Main crate: `packages/d2b-session-unix/`. Not present on v3 baseline. |----------------|----------------------|------------------|----------------|-------------------------------| | `d2b-session-unix/src/adapter.rs` | `UnixSeqpacketTransport`, `PeerIdentityPolicy` (accepted/pathname/inherited_socketpair variants), `UnixAttachmentPayload`, `OwnedUnixAttachment` | AF_UNIX SOCK_SEQPACKET transport implementing `OwnedTransport`; `SO_PEERCRED` identity policy; FD ancillary data ownership | `packages/d2b-session-unix/src/adapter.rs` | `PeerIdentityPolicy::inherited_socketpair` for testing: retain. Do not assume main's specific socket path constants. | | `d2b-session-unix/src/socket.rs` | `AncillaryCapacity`, `OutboundPacket`, `PacketBurst`, `SentPacket`, all send/receive helpers | Ancillary FD capacity calculation from `AttachmentPolicy`; packet batching; `cmsg` ancillary layout | `packages/d2b-session-unix/src/socket.rs` | No domain assumptions; depends on `v2_component_session` `AttachmentPolicy`; copy together | -| `d2b-session-unix/src/vsock.rs` | `FramedVsockTransport`, `NativeVsockTransport`, `NativeVsockListener` | Framed vsock implementing `OwnedTransport`; used for guest-control transport to guestd | `packages/d2b-session-unix/src/vsock.rs` | Replace vsock port constants (use v3 bundle-configured ports, not main hardcoded values) | +| `d2b-session-unix/src/vsock.rs` | `FramedVsockTransport`, `NativeVsockTransport`, `NativeVsockListener` | Framed vsock implementing `OwnedTransport`; used by the enrolled ComponentSession transport | `packages/d2b-session-unix/src/vsock.rs` | Replace vsock port constants (use v3 bundle-configured ports, not main hardcoded values) | | `d2b-session-unix/src/descriptor.rs` | `PeerCredentials`, `FirstPacketCredentials`, `ObjectIdentity`, `PidfdIdentityPolicy`, `DescriptorPolicy`, `AcceptedAttachment` | Received FD classification; pidfd identity binding; first-packet directional credential verification | `packages/d2b-session-unix/src/descriptor.rs` | No domain assumptions; clean copy | | `d2b-session-unix/src/credit.rs` | `CreditPool`, `CreditScopeSet`, `CreditBundle`, `CreditError`, `CreditScope` | Multi-scope attachment credit reservations; atomic rollback on partial failure; `MAX_PROCESS_ATTACHMENT_CREDITS` / `MAX_HOST_ATTACHMENT_CREDITS` from wire contract | `packages/d2b-session-unix/src/credit.rs` | Limits reference `v2_component_session` constants; copy both | | `d2b-session-unix/src/pidfd.rs` | `PidfdEvidence`, `PidfdIdentityVerifier` trait, `PidfdInfoSource` trait, `ProcPidfdIdentityVerifier`, `parse_pidfd_fdinfo`, `DigestEvidenceCallback` | Pidfd identity proof via `/proc/self/fd/` fdinfo parsing; fail-closed on parse error | `packages/d2b-session-unix/src/pidfd.rs` | No domain assumptions; clean copy | diff --git a/docs/specs/ADR-046-nix-configuration.md b/docs/specs/ADR-046-nix-configuration.md index dae2b58cc6..cdf62b92d8 100644 --- a/docs/specs/ADR-046-nix-configuration.md +++ b/docs/specs/ADR-046-nix-configuration.md @@ -12,6 +12,12 @@ | Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-provider-model-and-packaging`, `ADR-046-core-controllers` | | Supersedes | Current `nixos-modules/options-realms*.nix`, `options-envs.nix`, `options-vms.nix`, `index.nix`, `bundle*.nix`, `*-json.nix`, and generated `/etc/d2b/*.json` | +> **Current-state note (U10).** The baseline migration rows below predate the +> clean-break removal of the standalone Guest daemon and guest-control +> modules. Current Nix emits `d2bd guest`, the Guest-profile broker, +> ComponentSession enrollment, and signed Provider Process inputs; deleted +> baseline rows remain historical evidence only. + ## Source, reuse, and evidence policy This spec uses `b5ddbed6` as its authoritative factual baseline. diff --git a/docs/specs/ADR-046-provider-model-and-packaging.md b/docs/specs/ADR-046-provider-model-and-packaging.md index 0b5d8e9243..1aa252d07a 100644 --- a/docs/specs/ADR-046-provider-model-and-packaging.md +++ b/docs/specs/ADR-046-provider-model-and-packaging.md @@ -305,6 +305,8 @@ Descriptor fields include: - component ID/type/binary/template; - exported ResourceTypes/methods; - supported Host/Guest Provider capabilities; +- closed controller instance scope and supported target kinds; +- concrete per-target component artifact and required EffectPort classes; - allowed `system|user` domains; - cardinality; - config projection; @@ -436,6 +438,16 @@ compiles an offline sorted exact-digest catalog: - support channel; - support contact. +The signed manifest portion of the catalog additionally carries the closed +placement contract: each controller role's `instanceScope` and +`supportedTargetKinds`, each component's per-target `targetCapabilities` +(concrete component artifact digest plus required EffectPort classes), each +owned ResourceType's `placementAnchor`, and the target runtime's `d2bdDigest` +and `brokerDigest`. Host and Guest entries for one OS/architecture MUST use +identical daemon and broker digests. These fields are generated as +`placementContractFields` and `runtimeContractFields` in the catalog shape; +they are not free-form Provider selectors. + Selection is exact digest. No runtime marketplace, download, PATH scan, directory discovery, latest, or version-range solving. diff --git a/docs/specs/ADR-046-reset-and-cutover.md b/docs/specs/ADR-046-reset-and-cutover.md index 6bc18f47a3..1617db55be 100644 --- a/docs/specs/ADR-046-reset-and-cutover.md +++ b/docs/specs/ADR-046-reset-and-cutover.md @@ -4,14 +4,18 @@ | --- | --- | | Spec ID | `ADR-046-reset-and-cutover` | | Parent | ADR 0046 | -| Status | Accepted | +| Status | Superseded (2026-08-25; feature removed before release) | | Version | 1 | | Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | -| Normative | Yes | +| Normative | No - historical decision record | | Owners | Zone runtime bootstrap owner, `d2b` CLI cutover owner, storage/broker integrator, `d2b-resource-store-redb` owner | | Depends on | `ADR-046-decision-register`, `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-store-redb`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-provider-state`, `ADR-046-components-processes-and-sandbox`, `ADR-046-primitive-resource-composition`, `ADR-046-resources-volume`, `ADR-046-resources-device`, `ADR-046-resources-network`, `ADR-046-resources-host-guest-process-user`, `ADR-046-resources-credential`, `ADR-046-resources-zone-control`, `ADR-046-zone-routing`, `ADR-046-componentsession-and-bus`, `ADR-046-nix-configuration`, `ADR-046-cli-and-operations`, `ADR-046-telemetry-audit-and-support`, `ADR-046-core-controllers`, `ADR-046-current-code-migration-map` | | Supersedes | [ADR 0034](../adr/0034-storage-lifecycle-restart-and-synchronization.md) "Migration decision" section for the d2b 3.0 cutover; the current `d2b host migrate-storage` verb (retired per `ADR-046-current-code-migration-map` §6 and `ADR-046-cli-and-operations` "v2 command surface removed at 3.0 clean break": it served the one-time v1→v2 storage layout cutover only and has no v3 successor) | +> Historical record only. The host cutover and scoped-reset feature described +> below was removed before release. No CLI, daemon, broker, runner, or +> migration path is provided for this design. + ## Purpose This spec is the exhaustive normative contract for taking one running d2b host diff --git a/docs/specs/ADR-046-resources-volume.md b/docs/specs/ADR-046-resources-volume.md index fece4326d0..6025dc446c 100644 --- a/docs/specs/ADR-046-resources-volume.md +++ b/docs/specs/ADR-046-resources-volume.md @@ -314,7 +314,6 @@ time and re-resolves on any User resource revision change that affects the UID b | `boot` | Removed on next host/Zone boot; entry is /run/-scoped | `CleanupPolicy::Boot` | | `process-exit-with-proof` | Removed after the owning Process exits (verified by pidfd) | `CleanupPolicy::ProcessExitWithProof` | | `vm-stop-with-proof` | Removed when the owning Guest stops (verified by controller) | `CleanupPolicy::VmStopWithProof` | -| `cutover-only` | Removed on cutover/generation switch | `CleanupPolicy::CutoverOnly` | | `owner-controlled` | Lifecycle is owned by the controller that mounts/creates the Volume | `CleanupPolicy::External` | ### AdoptionPolicy @@ -687,13 +686,13 @@ enforces: | Entry path (relative) | Type | Invariants | Notes | | --- | --- | --- | --- | | `` (root) | directory | no-symlink, scope-authorization-required | root: `d2bd:users 0755` | -| `live` | directory | no-symlink, broker-opaque-id-only | hardlink farm root; `cutover-only` cleanup | +| `live` | directory | no-symlink, broker-opaque-id-only | hardlink farm root; `never` cleanup | | `live/.d2b-marker-` | file | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | zero-length readiness marker; `d2bd:users 0444` | | `meta` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | generation meta tree; guest-served via virtiofsd | -| `meta/generations` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | cutover-only cleanup | +| `meta/generations` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | never cleanup | | `meta/current` | symlink | broker-opaque-id-only | noFollow: false; points at generations/ | | `state` | directory | no-symlink, broker-opaque-id-only | host-only; NOT guest-served; `d2bd:users 0700`; holds `state/generations//` | -| `gcroots` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | host-only, at store-view root (NOT under `meta/`); cutover-only cleanup; `d2bd:users 0755` | +| `gcroots` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | host-only, at store-view root (NOT under `meta/`); never cleanup; `d2bd:users 0755` | | `sync.lock` | file | no-symlink, broker-opaque-id-only | OFD lock; never unlink; leaseClass: none | **Spec correction**: `nixos-modules/storage-json.nix` (baseline `b5ddbed6`) declares @@ -1132,7 +1131,7 @@ The virtiofsd worker is tested by: | `path:vm-state:` | `$storeStateDir/` | Volume root for per-Guest state | Guest owns as Volume with `kind: state` | | `path:vm-run:` | `/run/d2b/vms/` | Zone/Guest runtime root directory; not a Volume | system-core Host runtime path; volume-virtiofs stores export sockets here as a private implementation detail | | `path:store-view:` | `$storeStateDir//store-view` | `Volume/store-view-` root | volume-local, kind: durable | -| `path:store-view-live:` | `.../store-view/live` | LayoutEntry `live` | cutover-only, broker-opaque-id-only | +| `path:store-view-live:` | `.../store-view/live` | LayoutEntry `live` | never, broker-opaque-id-only | | `path:store-view-marker:` | `.../store-view/live/.d2b-marker-` | LayoutEntry `live/.d2b-marker-` | zero-length file, hardlink-farm invariants | | `path:store-view-meta:` | `.../store-view/meta` | LayoutEntry `meta` | same-filesystem, hardlink-farm; guest-served | | `path:store-view-generations:` | `.../store-view/meta/generations` | LayoutEntry `meta/generations` | same | @@ -1484,13 +1483,13 @@ d2b.zones."dev".resources."store-view-work-vm" = { kind = "durable"; layout = [ { path = ""; type = "directory"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } - { path = "live"; type = "directory"; invariants = [ "no-symlink" "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "live"; type = "directory"; invariants = [ "no-symlink" "broker-opaque-id-only" ]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } { path = "live/.d2b-marker-work-vm"; type = "file"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0444"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; repairPolicy = "exact-owner"; } { path = "meta"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } - { path = "meta/generations"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } - { path = "meta/current"; type = "symlink"; target = "generations/0"; noFollow = false; invariants = [ "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0777"; } + { path = "meta/generations"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "meta/current"; type = "symlink"; target = "generations/0"; noFollow = false; invariants = [ "broker-opaque-id-only" ]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0777"; } { path = "state"; type = "directory"; invariants = [ "no-symlink" "broker-opaque-id-only" ]; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0700"; } # host-only; NOT guest-served - { path = "gcroots"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } # at store-view root, NOT under meta/ + { path = "gcroots"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } # at store-view root, NOT under meta/ { path = "sync.lock"; type = "file"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0640"; leaseClass = "none"; invariants = [ "no-symlink" "broker-opaque-id-only" ]; restartPolicy = "preserve-across-controller-restart"; } ]; views = { diff --git a/docs/specs/ADR-046-security-and-threat-model.md b/docs/specs/ADR-046-security-and-threat-model.md index cd0ea1dfb0..a135fc5977 100644 --- a/docs/specs/ADR-046-security-and-threat-model.md +++ b/docs/specs/ADR-046-security-and-threat-model.md @@ -12,6 +12,12 @@ | Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-resource-store-redb`, `ADR-046-componentsession-and-bus`, `ADR-046-zone-routing`, `ADR-046-resources-zone-control`, `ADR-046-core-controllers`, `ADR-046-provider-model-and-packaging`, `ADR-046-provider-state`, `ADR-046-components-processes-and-sandbox`, `ADR-046-primitive-resource-composition`, `ADR-046-resources-host-guest-process-user`, `ADR-046-resources-volume`, `ADR-046-resources-network`, `ADR-046-resources-device`, `ADR-046-resources-credential`, `ADR-046-nix-configuration`, `ADR-046-cli-and-operations`, `ADR-046-telemetry-audit-and-support`, `ADR-046-current-code-migration-map`, `ADR-046-decision-register`, and all 27 `docs/specs/providers/ADR-046-provider-*.md` dossiers | | Supersedes | None. This spec is a new cross-cutting synthesis. `SECURITY.md` and `docs/explanation/design.md` remain the disclosure policy and pre-v3 (v1/v2) threat model for the protected `v3` baseline and are not superseded; historical main-branch ADR 0045 (`a1cc0b2d^:docs/adr/0045-provider-and-transport-framework.md`, "d2b 2.0 provider and transport framework") and ADR 0032/0034 remain non-ancestry reuse sources whose useful invariants this spec adapts under D001/D041 | +> **Current-state note (U10).** The baseline threat rows that name the +> standalone Guest daemon, feature-specific guest-control HMACs, or token-share +> broker path are historical. The implemented boundary is the enrolled +> ComponentSession plus signed target-local Process and Guest-profile broker; +> old peers are rejected before feature or authority state is exposed. + This spec is the normative closing security and threat-model contract for the ADR 0046 d2b 3.0 Provider control plane. It does not introduce new ResourceTypes, Providers, or wire contracts. It collects, cross-references, and diff --git a/docs/specs/README.md b/docs/specs/README.md index 53cd7aaf08..22ffc14a6b 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -3,8 +3,10 @@ This directory contains the surviving normative specifications for [ADR 0046](../adr/0046-d2b-3-provider-control-plane.md). The parent decision and these Markdown files are the source of truth for the planned provider -control plane. Contributor workflow, delivery records, and generated -validation indexes are not part of this set. +control plane. The retired reset-and-cutover specification remains in the +repository as historical decision context, but is not a current contract. +Contributor workflow, delivery records, and generated validation indexes are +not part of this set. ## Foundation and platform @@ -24,7 +26,6 @@ validation indexes are not part of this set. - [Nix configuration](ADR-046-nix-configuration.md) - [Current-code migration map](ADR-046-current-code-migration-map.md) - [Feasibility and spikes](ADR-046-feasibility-and-spikes.md) -- [Reset and cutover](ADR-046-reset-and-cutover.md) ## Resource catalog @@ -54,7 +55,7 @@ Each specification begins with a metadata table containing: | --- | --- | | Spec ID | Stable `ADR-046-` identifier | | Parent | `ADR 0046` | -| Status | `Proposed` or `Accepted` | +| Status | `Proposed`, `Accepted`, or `Superseded` for historical records | | Version | Monotonic integer | | Baseline | Exact source revision analyzed | | Normative | `Yes` | diff --git a/docs/specs/providers/ADR-046-provider-audio-pipewire.md b/docs/specs/providers/ADR-046-provider-audio-pipewire.md index 794d212999..40f8dac7f4 100644 --- a/docs/specs/providers/ADR-046-provider-audio-pipewire.md +++ b/docs/specs/providers/ADR-046-provider-audio-pipewire.md @@ -12,6 +12,11 @@ | Depends on | `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-resources-volume`, `ADR-046-provider-state`, `ADR-046-componentsession-and-bus`, `ADR-046-nix-configuration`, `ADR-046-telemetry-audit-and-support`, `ADR-046-resources-host-guest-process-user` | | Supersedes | `nixos-modules/components/audio/host.nix`, `nixos-modules/components/audio/guest.nix`, `packages/d2b-core/src/audio_policy.rs`, `packages/d2bd/src/audio_dispatch.rs`, `packages/d2bd/src/audio_host_controller.rs`, `packages/d2b-host/src/audio_argv.rs` | +> **Current-state note (U10).** Baseline rows naming the deleted Guest daemon, +> guest-control bridge, or `guestControl.*` Nix options are historical. The +> active guest-side effect owner is the signed target-local audio Process and +> its typed ComponentSession service. + ## Purpose This spec exhaustively defines the `audio-pipewire` Provider for d2b 3.0. It diff --git a/docs/specs/providers/ADR-046-provider-device-usbip.md b/docs/specs/providers/ADR-046-provider-device-usbip.md index 93070c69c6..4e007f8036 100644 --- a/docs/specs/providers/ADR-046-provider-device-usbip.md +++ b/docs/specs/providers/ADR-046-provider-device-usbip.md @@ -12,6 +12,11 @@ | Depends on | `ADR-046-resources-device`, `ADR-046-resources-network`, `ADR-046-resources-zone-control`, `ADR-046-zone-routing`, `ADR-046-components-processes-and-sandbox`, `ADR-046-provider-model-and-packaging`, `ADR-046-resource-reconciliation`, `ADR-046-telemetry-audit-and-support`, `ADR-046-componentsession-and-bus`, `ADR-046-nix-configuration`, `ADR-046-resource-api-and-authorization`, `ADR-046-provider-state` | | Supersedes | `nixos-modules/components/usbip.nix` (host-side), per-env usbipd systemd units in `nixos-modules/network.nix`, `ProcessRole::Usbip` / `RunnerRole::Usbip` in current v3 baseline | +> **Current-state note (U10).** Guest import and detach are owned by the +> signed target-local USBIP Process over ComponentSession. The baseline +> guest-control and `guestControl.usbipPath` references below are historical; +> host bind, firewall, proxy, and claim ownership remain in this Provider. + --- ## Purpose diff --git a/docs/specs/providers/ADR-046-provider-transport-vsock.md b/docs/specs/providers/ADR-046-provider-transport-vsock.md index 31b5b50383..e80b889e93 100644 --- a/docs/specs/providers/ADR-046-provider-transport-vsock.md +++ b/docs/specs/providers/ADR-046-provider-transport-vsock.md @@ -12,6 +12,12 @@ | Depends on | `ADR-046-terminology-and-identities`, `ADR-046-componentsession-and-bus`, `ADR-046-zone-routing`, `ADR-046-provider-model-and-packaging`, `ADR-046-resources-zone-control`, `ADR-046-resource-api-and-authorization`, `ADR-046-resources-host-guest-process-user`, `ADR-046-provider-state` | | Supersedes | `packages/d2bd/src/guest_control_vsock.rs` transport probe (→ allocator-issued endpoint via VsockEffectPort); `packages/d2b-host/src/vsock_relay_argv.rs` socat relay (→ native FramedVsockTransport in Provider service); `NativeVsock`/`CloudHypervisorVsock` in `d2b-session-unix/src/vsock.rs` (→ FramedVsockTransport implementing OwnedTransport adapted as service-Provider transport bridge) | +> **Current-state note (U10).** The baseline Guest-control transport rows are +> historical disposition records. The active enrolled transport helper is +> `packages/d2bd-runtime/src/component_session_vsock.rs`; it carries only the +> ComponentSession connection setup and delegates identity/authentication to +> the session engine. No legacy Guest protocol or token-share path remains. + ## Source and reuse policy The pre-ADR-0045 v3 baseline has no generic vsock transport Provider or diff --git a/docs/specs/providers/ADR-046-provider-volume-local.md b/docs/specs/providers/ADR-046-provider-volume-local.md index 91db35d39e..76c373bf7d 100644 --- a/docs/specs/providers/ADR-046-provider-volume-local.md +++ b/docs/specs/providers/ADR-046-provider-volume-local.md @@ -334,7 +334,6 @@ that affects the UID binding. | `boot` | Removed on next host/Zone boot; entry is `/run/`-scoped | `CleanupPolicy::Boot` | | `process-exit-with-proof` | Removed after owning Process exits (verified by pidfd) | `CleanupPolicy::ProcessExitWithProof` | | `vm-stop-with-proof` | Removed when owning Guest stops (verified by controller) | `CleanupPolicy::VmStopWithProof` | -| `cutover-only` | Removed on cutover/generation switch | `CleanupPolicy::CutoverOnly` | | `owner-controlled` | Lifecycle owned by the mounting controller | `CleanupPolicy::External` | ### AdoptionPolicy @@ -567,7 +566,7 @@ spec: noFollow: true invariants: [no-symlink, broker-opaque-id-only] createPolicy: create-if-absent - cleanupPolicy: cutover-only + cleanupPolicy: never - path: live/.d2b-marker- type: file ownerRef: User/d2bd @@ -589,7 +588,7 @@ spec: groupRef: User/users mode: "0755" invariants: [no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only] - cleanupPolicy: cutover-only + cleanupPolicy: never - path: meta/current type: symlink ownerRef: User/d2bd @@ -612,7 +611,7 @@ spec: groupRef: User/users mode: "0755" invariants: [no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only] - cleanupPolicy: cutover-only + cleanupPolicy: never # host-only; at store-view root (NOT under meta/); see spec correction below - path: sync.lock type: file diff --git a/docs/specs/providers/ADR-046-provider-volume-virtiofs.md b/docs/specs/providers/ADR-046-provider-volume-virtiofs.md index 94a71422b1..6b6b066d25 100644 --- a/docs/specs/providers/ADR-046-provider-volume-virtiofs.md +++ b/docs/specs/providers/ADR-046-provider-volume-virtiofs.md @@ -1271,13 +1271,13 @@ d2b.zones."dev".resources."store-view-work-vm" = { kind = "durable"; layout = [ { path = ""; type = "directory"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } - { path = "live"; type = "directory"; invariants = ["no-symlink" "broker-opaque-id-only"]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "live"; type = "directory"; invariants = ["no-symlink" "broker-opaque-id-only"]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } { path = "live/.d2b-marker-work-vm"; type = "file"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0444"; invariants = ["no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only"]; repairPolicy = "exact-owner"; } { path = "meta"; type = "directory"; invariants = ["no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only"]; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } - { path = "meta/generations"; type = "directory"; invariants = ["no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only"]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } - { path = "meta/current"; type = "symlink"; target = "generations/0"; noFollow = false; invariants = ["broker-opaque-id-only"]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0777"; } + { path = "meta/generations"; type = "directory"; invariants = ["no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only"]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "meta/current"; type = "symlink"; target = "generations/0"; noFollow = false; invariants = ["broker-opaque-id-only"]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0777"; } { path = "state"; type = "directory"; invariants = ["no-symlink" "broker-opaque-id-only"]; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0700"; } - { path = "gcroots"; type = "directory"; invariants = ["no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only"]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "gcroots"; type = "directory"; invariants = ["no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only"]; cleanupPolicy = "never"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } { path = "sync.lock"; type = "file"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0640"; leaseClass = "none"; invariants = ["no-symlink" "broker-opaque-id-only"]; restartPolicy = "preserve-across-controller-restart"; } ]; views = { diff --git a/examples/graphics-workstation/README.md b/examples/graphics-workstation/README.md index 508f7dd04f..5db80b7ee1 100644 --- a/examples/graphics-workstation/README.md +++ b/examples/graphics-workstation/README.md @@ -156,8 +156,8 @@ talks to PipeWire as a regular client, so: - **Guest side** (from `usbip.yubikey = true`): - The `vhci_hcd` kernel module is loaded so the guest has a virtual USB host controller. - - The `usbip` userspace CLI is installed for authenticated guestd - import/detach operations. + - The `usbip` userspace CLI is installed for authenticated target-local + Process import/detach operations. Declare the approved busid in the copied example and use the read-only probe to confirm host observation: @@ -229,7 +229,7 @@ implicitly by the host activation script) produces: bridge - workload VMs cannot reach the host's neighbours. - **Root-visible services:** `d2bd.service`, - `d2b-priv-broker.socket`, and `d2b-priv-broker.service`. + `d2b-broker.socket`, and `d2b-broker.service`. Per-VM runners (cloud-hypervisor, virtiofsd, swtpm, GPU/audio/USBIP sidecars) are broker-spawned and supervised by `d2bd`, not by per-VM systemd templates. diff --git a/examples/graphics-workstation/configuration.nix b/examples/graphics-workstation/configuration.nix index 83717c2e80..1076532199 100644 --- a/examples/graphics-workstation/configuration.nix +++ b/examples/graphics-workstation/configuration.nix @@ -141,7 +141,7 @@ graphics.crossDomainTrusted = true; audio.enable = true; # vhost-user-sound → host PipeWire usbip.yubikey = true; # `d2b device usb attach corp-desktop --apply` - guest.control.enable = true; # guestd owns guest-side USBIP import + guest.componentSession.enable = true; # target-local Process owns USBIP import # Audio grants are OFF by default. The host-side audio sidecar # is installed, but the per-VM state file at diff --git a/examples/minimal/configuration.nix b/examples/minimal/configuration.nix index 8bc2384cfd..6cbcb9bb79 100644 --- a/examples/minimal/configuration.nix +++ b/examples/minimal/configuration.nix @@ -78,7 +78,7 @@ index = 10; # `d2b activation switch Guest/personal-dev --apply` activates through - # the Guest resource and guest control + # the Guest resource and ComponentSession # using the framework-managed Ed25519 key generated under # /var/lib/d2b/keys/ on every activation. ssh.user = "alice"; diff --git a/flake.nix b/flake.nix index 5afc16fb62..107c8b4e97 100644 --- a/flake.nix +++ b/flake.nix @@ -140,23 +140,69 @@ }; providerElfShim = import ./nix/provider-elf-shim.nix; + # The Guest static workspace mirrors the shared daemon/broker dependency + # closure. Guest packaging contains only the shared daemon, broker, + # and signed Provider workspace inputs. mkGuestRustPackagesSrc = pkgs: pkgs.runCommand "d2b-guest-rust-src" { } '' mkdir -p $out/packages - cp -r ${./packages/d2b-realm-core} $out/packages/d2b-realm-core - cp -r ${./packages/d2b-core} $out/packages/d2b-core + cp -r ${./packages/d2b-audit} $out/packages/d2b-audit + cp -r ${./packages/d2b-broker} $out/packages/d2b-broker + cp -r ${./packages/d2b-bus} $out/packages/d2b-bus cp -r ${./packages/d2b-contracts} $out/packages/d2b-contracts + cp -r ${./packages/d2b-contracts-broker} $out/packages/d2b-contracts-broker cp -r ${./packages/d2b-contracts-control} $out/packages/d2b-contracts-control + cp -r ${./packages/d2b-contracts-provider} $out/packages/d2b-contracts-provider cp -r ${./packages/d2b-contracts-resource} $out/packages/d2b-contracts-resource - cp -r ${./packages/d2b-guestd} $out/packages/d2b-guestd - cp -r ${./packages/d2b-exec-runner} $out/packages/d2b-exec-runner + cp -r ${./packages/d2b-contracts-zone-session} $out/packages/d2b-contracts-zone-session + cp -r ${./packages/d2b-controller-toolkit} $out/packages/d2b-controller-toolkit + cp -r ${./packages/d2b-core} $out/packages/d2b-core + cp -r ${./packages/d2b-core-controller} $out/packages/d2b-core-controller + cp -r ${./packages/d2b-gateway} $out/packages/d2b-gateway + cp -r ${./packages/d2b-gateway-runtime} $out/packages/d2b-gateway-runtime + cp -r ${./packages/d2b-host} $out/packages/d2b-host cp -r ${./packages/d2b-sk-frontend} $out/packages/d2b-sk-frontend + cp -r ${./packages/d2b-process} $out/packages/d2b-process + cp -r ${./packages/d2b-process-conformance} $out/packages/d2b-process-conformance + cp -r ${./packages/d2b-provider} $out/packages/d2b-provider + cp -r ${./packages/d2b-provider-activation-nixos} $out/packages/d2b-provider-activation-nixos + cp -r ${./packages/d2b-provider-audio-pipewire} $out/packages/d2b-provider-audio-pipewire + cp -r ${./packages/d2b-provider-clipboard-wayland} $out/packages/d2b-provider-clipboard-wayland + cp -r ${./packages/d2b-provider-config-nixos} $out/packages/d2b-provider-config-nixos + cp -r ${./packages/d2b-provider-device-security-key} $out/packages/d2b-provider-device-security-key + cp -r ${./packages/d2b-provider-device-tpm} $out/packages/d2b-provider-device-tpm + cp -r ${./packages/d2b-provider-device-usbip} $out/packages/d2b-provider-device-usbip + cp -r ${./packages/d2b-provider-display-wayland} $out/packages/d2b-provider-display-wayland + cp -r ${./packages/d2b-provider-network-local} $out/packages/d2b-provider-network-local + cp -r ${./packages/d2b-provider-notification-desktop} $out/packages/d2b-provider-notification-desktop + cp -r ${./packages/d2b-provider-observability-otel} $out/packages/d2b-provider-observability-otel + cp -r ${./packages/d2b-provider-runtime-azure-container-apps} $out/packages/d2b-provider-runtime-azure-container-apps + cp -r ${./packages/d2b-provider-runtime-cloud-hypervisor} $out/packages/d2b-provider-runtime-cloud-hypervisor + cp -r ${./packages/d2b-provider-shell-terminal} $out/packages/d2b-provider-shell-terminal + cp -r ${./packages/d2b-provider-supervisor} $out/packages/d2b-provider-supervisor + cp -r ${./packages/d2b-provider-system-core} $out/packages/d2b-provider-system-core + cp -r ${./packages/d2b-provider-system-minijail} $out/packages/d2b-provider-system-minijail + cp -r ${./packages/d2b-provider-system-systemd} $out/packages/d2b-provider-system-systemd + cp -r ${./packages/d2b-provider-toolkit} $out/packages/d2b-provider-toolkit + cp -r ${./packages/d2b-provider-transport-azure-relay} $out/packages/d2b-provider-transport-azure-relay + cp -r ${./packages/d2b-provider-volume-local} $out/packages/d2b-provider-volume-local + cp -r ${./packages/d2b-realm-core} $out/packages/d2b-realm-core + cp -r ${./packages/d2b-resource-api} $out/packages/d2b-resource-api + cp -r ${./packages/d2b-resource-store} $out/packages/d2b-resource-store + cp -r ${./packages/d2b-resource-store-redb} $out/packages/d2b-resource-store-redb + cp -r ${./packages/d2b-session} $out/packages/d2b-session + cp -r ${./packages/d2b-session-unix} $out/packages/d2b-session-unix + cp -r ${./packages/d2b-telemetry} $out/packages/d2b-telemetry + cp -r ${./packages/d2b-zone-routing} $out/packages/d2b-zone-routing + cp -r ${./packages/d2bd} $out/packages/d2bd + cp -r ${./packages/d2bd-runtime} $out/packages/d2bd-runtime + mkdir -p $out/docs/reference/schemas/v3/providers + cp ${./docs/reference/schemas/v3/providers/transport-azure-relay.transport-settings.json} \ + $out/docs/reference/schemas/v3/providers/transport-azure-relay.transport-settings.json + cp ${./docs/reference/schemas/v3/providers/transport-vsock.transport-binding.json} \ + $out/docs/reference/schemas/v3/providers/transport-vsock.transport-binding.json cp ${./packages/Cargo.guest.lock} $out/packages/Cargo.lock - chmod -R u+w $out/packages/d2b-core - chmod -R u+w $out/packages/d2b-contracts - chmod -R u+w $out/packages/d2b-contracts-control - chmod -R u+w $out/packages/d2b-contracts-resource - chmod -R u+w $out/packages/d2b-realm-core + chmod -R u+w $out/packages cp ${./tests/fixtures/guest-rust-workspace/d2b-contracts.Cargo.toml} \ $out/packages/d2b-contracts/Cargo.toml cp ${./tests/fixtures/guest-rust-workspace/d2b-realm-core.Cargo.toml} \ @@ -219,7 +265,13 @@ mkBazelShellHook = testPath: '' export D2B_PROJECT_SHELL=d2b export D2B_BAZEL_BIN="${bazel920}/bin/bazel" - export BAZEL_SH="''${BAZEL_SH:-${bazelActionShell}/bin/bash}" + if [ -z "''${BAZEL_SH:-}" ]; then + if [ -x /bin/bash ]; then + export BAZEL_SH=/bin/bash + else + export BAZEL_SH="${bazelActionShell}/bin/bash" + fi + fi export D2B_SHELLCHECK_BIN="${pkgs.shellcheck}/bin/shellcheck" export D2B_BAZEL_TEST_PATH="${testPath}" ''; @@ -366,7 +418,10 @@ version = "0.0.0-bootstrap"; src = guestRustPackagesSrc; sourceRoot = "d2b-guest-rust-src/packages"; - inherit cargoLock; + cargoLock = { + lockFile = ./packages/Cargo.guest.lock; + outputHashes."wl-proxy-0.1.2" = "sha256-1yO1zgzSyzQ2DnDMpVxcnI5BsTNvXfzIUS+RNlPj4A8="; + }; cargoBuildFlags = [ "--package" packageName "--bin" binName ]; doCheck = false; RUSTC_WRAPPER = ""; @@ -468,9 +523,9 @@ install -Dm644 ${./completions/d2b.zsh} "$out/share/zsh/site-functions/_d2b" install -Dm644 ${./completions/d2b.fish} "$out/share/fish/vendor_completions.d/d2b.fish" ''; - d2b-guestd-static = guestStaticPackage "d2b-guestd" "d2b-guestd"; - d2b-exec-runner-static = - guestStaticPackage "d2b-exec-runner" "d2b-exec-runner"; + d2bd-guest-static = guestStaticPackage "d2bd" "d2bd"; + d2b-broker-guest-static = + guestStaticPackage "d2b-broker" "d2b-broker"; d2b-sk-frontend-static = guestStaticPackage "d2b-sk-frontend" "d2b-sk-frontend"; d2b-guest-shell-runner-static = guestShellRunnerStatic; @@ -576,7 +631,7 @@ # userland - e.g. proving a static d2b binary runs on stock Ubuntu. # It deliberately does NOT boot systemd for daemon/socket activation; # that is covered natively by - # packages/d2b-priv-broker/tests/socket_activation.rs plus nix-unit. + # packages/d2b-broker/tests/socket_activation.rs plus nix-unit. # See tests/integration/containers/README.md. # # Auto-discovered from tests/integration/containers/images/*.nix: each image module is @@ -956,7 +1011,7 @@ graphics.videoSidecar = true; audio.enable = true; usbip.yubikey = true; - guest.control.enable = true; + guest.componentSession.enable = true; tpm.enable = true; observability.enable = true; config = { @@ -1136,7 +1191,7 @@ } // args); rustToolchainChannel = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)).toolchain.channel; - brokerManifestToml = builtins.fromTOML (builtins.readFile ./packages/d2b-priv-broker/Cargo.toml); + brokerManifestToml = builtins.fromTOML (builtins.readFile ./packages/d2b-broker/Cargo.toml); mainManifestToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); assertRustToolchain = '' rustc --version | grep -F "${rustToolchainChannel}" @@ -1381,10 +1436,15 @@ guest-rust-deny = let guestVendor = pkgs.rustPlatform.importCargoLock { lockFile = ./packages/Cargo.guest.lock; + outputHashes."wl-proxy-0.1.2" = "sha256-1yO1zgzSyzQ2DnDMpVxcnI5BsTNvXfzIUS+RNlPj4A8="; }; cargoConfig = '' [source.crates-io] replace-with = "vendored-sources" + [source."git+https://github.com/vicondoa/wl-proxy.git?rev=072945b59fef21a2a8166460454280d543f48772#072945b59fef21a2a8166460454280d543f48772"] + git = "https://github.com/vicondoa/wl-proxy.git" + rev = "072945b59fef21a2a8166460454280d543f48772" + replace-with = "vendored-sources" [source.vendored-sources] directory = "${guestVendor}" ''; diff --git a/labs/wlattach/deny.toml b/labs/wlattach/deny.toml index 34c4109d33..a4267083b9 100644 --- a/labs/wlattach/deny.toml +++ b/labs/wlattach/deny.toml @@ -1,5 +1,5 @@ # Standalone supply-chain config, wired into the per-manifest `cargo deny` loop -# in tests/test-rust.sh alongside d2b-priv-broker and d2b-guest-shell-runner. +# in tests/test-rust.sh alongside d2b-broker and d2b-guest-shell-runner. [advisories] version = 2 yanked = "deny" diff --git a/labs/wlattach/src/serve/sys.rs b/labs/wlattach/src/serve/sys.rs index be9bd9d333..a4136e1718 100644 --- a/labs/wlattach/src/serve/sys.rs +++ b/labs/wlattach/src/serve/sys.rs @@ -1,6 +1,6 @@ //! The crate's **only** `unsafe` code. //! -//! Quarantined here deliberately, following the `d2b-priv-broker` precedent of +//! Quarantined here deliberately, following the `d2b-broker` precedent of //! `unsafe_code = "deny"` plus one audited module, so every unsafe expression in //! the prototype is reviewable in a single short file. //! diff --git a/nix/prebuilt.json b/nix/prebuilt.json index 85e5a88caa..d36c17bb94 100644 --- a/nix/prebuilt.json +++ b/nix/prebuilt.json @@ -10,9 +10,10 @@ "url": "https://github.com/vicondoa/d2b/releases/download/v1.4.1/d2b-activation-helper-1.4.1-x86_64-linux.tar.gz", "hash": "sha256-krP84+rfDOi/7EghxtPqjIWMx9Klryz9JLFMPZnGV8M=" }, - "d2b-priv-broker": { + "d2b-broker": { "url": "https://github.com/vicondoa/d2b/releases/download/v1.4.1/d2b-priv-broker-1.4.1-x86_64-linux.tar.gz", - "hash": "sha256-qY5eXFpenVG6UrgwwV4nQC+QQxFr3O+mJHVdOSj256A=" + "hash": "sha256-qY5eXFpenVG6UrgwwV4nQC+QQxFr3O+mJHVdOSj256A=", + "sourceBinary": "d2b-priv-broker" }, "d2b-unsafe-local-helper": { "url": "https://github.com/vicondoa/d2b/releases/download/v1.4.1/d2b-unsafe-local-helper-1.4.1-x86_64-linux.tar.gz", diff --git a/nix/prebuilt.nix b/nix/prebuilt.nix index 1cc9fc8994..4fe1044ea2 100644 --- a/nix/prebuilt.nix +++ b/nix/prebuilt.nix @@ -3,6 +3,8 @@ # Reads nix/prebuilt.json and provides pre-built binary derivations. # Uses autoPatchelfHook to fix library paths for the consumer's nixpkgs. # Returns null when no release is available (callers fall back to source). +# A manifest entry may set `sourceBinary` when a released asset still carries +# a legacy executable name that the consumer package must rename. let manifest = builtins.fromJSON (builtins.readFile ./prebuilt.json); @@ -11,6 +13,24 @@ let && builtins.length (builtins.attrNames manifest.binaries) > 0; mkPrebuilt = name: spec: + let + sourceBinary = spec.sourceBinary or null; + installBinaries = + if sourceBinary != null then '' + candidate=./${sourceBinary} + if [ ! -f "$candidate" ] || [ ! -x "$candidate" ]; then + echo "prebuilt ${name}: expected executable ${sourceBinary}" >&2 + exit 1 + fi + install -Dm755 "$candidate" "$out/bin/${name}" + '' else '' + for candidate in ./*; do + if [ -f "$candidate" ] && [ -x "$candidate" ]; then + install -Dm755 "$candidate" "$out/bin/$(basename "$candidate")" + fi + done + ''; + in pkgs.stdenv.mkDerivation { pname = name; version = manifest.version; @@ -19,6 +39,7 @@ let }; nativeBuildInputs = [ pkgs.autoPatchelfHook ]; buildInputs = [ pkgs.stdenv.cc.cc.lib ]; + passthru = { inherit sourceBinary; }; dontConfigure = true; dontBuild = true; unpackPhase = '' @@ -29,11 +50,7 @@ let installPhase = '' runHook preInstall mkdir -p "$out/bin" - for candidate in ./*; do - if [ -f "$candidate" ] && [ -x "$candidate" ]; then - install -Dm755 "$candidate" "$out/bin/$(basename "$candidate")" - fi - done + ${installBinaries} runHook postInstall ''; meta.platforms = [ manifest.system ]; diff --git a/nix/test-support/bazel-host-tools.nix b/nix/test-support/bazel-host-tools.nix index 14afa3f100..afc2dbd3d5 100644 --- a/nix/test-support/bazel-host-tools.nix +++ b/nix/test-support/bazel-host-tools.nix @@ -6,10 +6,9 @@ let inventory = [ "d2b" "d2bd" - "d2b-priv-broker" + "d2b-broker" "d2b-activation-helper" "d2b-host-activation-helper" - "d2b-cutover-runner" "d2b-unsafe-local-helper" "d2b-resource-compiler" "d2b-wayland-proxy" @@ -21,7 +20,6 @@ let "broker" "activationHelper" "hostActivationHelper" - "cutoverRunner" "unsafeLocalHelper" "resourceCompiler" "waylandProxy" diff --git a/nixos-modules/artifact-catalog.nix b/nixos-modules/artifact-catalog.nix index 9fab6505fd..59ef432152 100644 --- a/nixos-modules/artifact-catalog.nix +++ b/nixos-modules/artifact-catalog.nix @@ -38,10 +38,10 @@ let }) ids; - # Keep the pre-cutover internal projection available to the legacy - # zone-resources emitter while the installed document uses the v3 artifact - # rows above. The compatibility view intentionally contains no new - # authority; it is only an eval-visible table for existing consumers. + # Keep the legacy internal projection available to the zone-resources + # emitter while the installed document uses the v3 artifact rows above. + # The compatibility view intentionally contains no new authority; it is + # only an eval-visible table for existing consumers. compatibilityEntries = map (entry: { inherit (entry) id type storePath; diff --git a/nixos-modules/assertions.nix b/nixos-modules/assertions.nix index db7072d778..b704603d6c 100644 --- a/nixos-modules/assertions.nix +++ b/nixos-modules/assertions.nix @@ -959,7 +959,7 @@ let message = '' d2b migration-required (legacy-surface-detected: d2b.gateways): `d2b.gateways` and its old gateway/ACA sandbox fields were removed as a - public configuration surface by the realm-native cutover. + public configuration surface by the realm-native migration. Move non-secret coordinates into the realm-native schema, for example: @@ -1117,7 +1117,7 @@ let (pair: socketPathTooLong pair.socket || socketPathTooLong "${pair.socket}_${toString d2bLib.observabilityOtlpVsockPort}" - || socketPathTooLong "${pair.socket}_${toString d2bLib.guestControlVsockPort}") + || socketPathTooLong "${pair.socket}_${toString d2bLib.componentSessionVsockPort}") vmVsockSocketPairs; vmAssertions = lib.mapAttrsToList @@ -1248,35 +1248,6 @@ let entrablau README for the full migration recipe. ''; } - { - # `d2b.vms..guest.exec.allowRoot` was removed: - # guest-control exec now ALWAYS runs as the VM's workload - # user (`ssh.user`) inside a PAM login session, never root. - # The option is a kept-but-internal stub (options-vms.nix) so - # legacy assignments land on this friendly message instead of - # a cryptic "option does not exist" module-system error. - assertion = !(vm.enable && vm.guest.exec.allowRoot); - message = '' - d2b.vms.${name}.guest.exec.allowRoot was removed. - Guest-control exec now always runs as the VM's workload - user (`ssh.user`) inside a PAM login session - never as - root. There is no root-exec mode. Remove - `guest.exec.allowRoot = ...;`; to run a command as root, - elevate with `sudo` inside the exec session. - ''; - } - { - # `d2b.vms..guest.exec.users` was removed: there is - # no per-VM exec user allowlist; exec always targets the - # single workload user (`ssh.user`). Kept-but-internal stub. - assertion = !(vm.enable && vm.guest.exec.users != [ ]); - message = '' - d2b.vms.${name}.guest.exec.users was removed. - Guest-control exec now always targets the VM's single - workload user (`ssh.user`); there is no per-VM exec user - allowlist. Remove `guest.exec.users = [ ... ];`. - ''; - } { assertion = let timeout = vm.lifecycle.gracefulShutdown.timeoutSeconds; @@ -1511,19 +1482,15 @@ let } { assertion = - !vm.guest.control.enable - && vm.guest.control.auth.tokenFile == null - && !vm.guest.exec.enable - && !vm.guest.exec.allowRoot - && vm.guest.exec.users == [ ] + !vm.guest.componentSession.enable && !vm.guest.shell.enable && vm.guest.shell.defaultName == "default" && vm.guest.shell.maxSessions == 8 && vm.guest.shell.maxAttached == 1; message = '' d2b.vms.${name}: runtime.kind = "qemu-media" is incompatible - with guest-control, guest exec, and persistent shell options. - Disable guest.control.*, guest.exec.*, and guest.shell.* for this + with ComponentSession, target-local execution, and persistent shell options. + Disable guest.componentSession.* and guest.shell.* for this manual-only runtime. ''; } diff --git a/nixos-modules/base.nix b/nixos-modules/base.nix index 0b231ec84e..7cafd33a11 100644 --- a/nixos-modules/base.nix +++ b/nixos-modules/base.nix @@ -106,7 +106,7 @@ in # Fires on every boot AND whenever the host publishes steady-state # store metadata by bumping `current` (via the path-trigger below). # Live `d2b switch ` additionally runs through authenticated - # guest-control activation before the broker commits the host-side + # component-session activation before the broker commits the host-side # current pointers. # --------------------------------------------------------------------------- systemd.services.d2b-load-store-db = { diff --git a/nixos-modules/bundle-artifacts.nix b/nixos-modules/bundle-artifacts.nix index 20bc6cc081..e80e1fec08 100644 --- a/nixos-modules/bundle-artifacts.nix +++ b/nixos-modules/bundle-artifacts.nix @@ -214,14 +214,6 @@ in description = "Internal typed privileges.json artifact metadata."; }; - cutoverRunnerPath = lib.mkOption { - type = types.nullOr types.str; - default = null; - internal = true; - visible = false; - description = "Internal trusted store path for the one-shot cutover runner."; - }; - storageJson = lib.mkOption { type = artifactModule; default = { }; diff --git a/nixos-modules/bundle.nix b/nixos-modules/bundle.nix index d6fd736760..2d1316f56e 100644 --- a/nixos-modules/bundle.nix +++ b/nixos-modules/bundle.nix @@ -118,7 +118,6 @@ let hostPath = "/etc/d2b/host.json"; processesPath = "/etc/d2b/processes.json"; privilegesPath = "/etc/d2b/privileges.json"; - cutoverRunnerPath = config.d2b._bundle.cutoverRunnerPath; storagePath = "/etc/d2b/storage.json"; syncPath = "/etc/d2b/sync.json"; allocatorPath = "/etc/d2b/allocator.json"; diff --git a/nixos-modules/closures-json.nix b/nixos-modules/closures-json.nix index 03bd216bcc..e5b27f469d 100644 --- a/nixos-modules/closures-json.nix +++ b/nixos-modules/closures-json.nix @@ -207,6 +207,18 @@ let closurePaths = [ "${artifact.package}" ]; }) v3SystemArtifactIds); + + guestClosureData = { + schemaVersion = 1; + entries = lib.sortOn (entry: "${entry.zone}/${entry.guest}") + (map + (entry: { + inherit (entry) artifactId guest relativePath zone; + }) + (lib.attrValues v3Closures)); + }; + guestClosureJson = builtins.toJSON guestClosureData; + guestClosurePath = pkgs.writeText "d2b-guest-closures.json" guestClosureJson; in { options.d2b._bundle.closuresV3 = lib.mkOption { @@ -236,6 +248,18 @@ in (_: closure: lib.nameValuePair "d2b/${closure.relativePath}" (privateEtc closure.path)) v3Closures) + (lib.mapAttrs' + (_: closure: + lib.nameValuePair "d2b/${closure.relativePath}" (privateEtc closure.file)) + v3ArtifactClosures) ]; + d2b._bundle.extraArtifacts.guestClosures = { + data = guestClosureData; + jsonText = guestClosureJson; + path = guestClosurePath; + installFileName = "guest-closures.json"; + classification = "contractPrivateNonSecret"; + sensitivity = "nonSecret"; + }; }; } diff --git a/nixos-modules/component-session.nix b/nixos-modules/component-session.nix new file mode 100644 index 0000000000..050445f39b --- /dev/null +++ b/nixos-modules/component-session.nix @@ -0,0 +1,345 @@ +{ config, lib, name, d2bInputs ? { }, d2bHostTools ? null, pkgs, ... }: + +let + cfg = config.d2b.componentSession; + system = pkgs.stdenv.hostPlatform.system; + self = + if builtins.isAttrs d2bInputs && builtins.hasAttr "self" d2bInputs + then d2bInputs.self + else { }; + flakePackages = + if builtins.isAttrs self + && builtins.hasAttr "packages" self + && builtins.hasAttr system self.packages + then self.packages.${system} + else { }; + packageFrom = name: + if d2bHostTools != null + && builtins.hasAttr name d2bHostTools + && builtins.getAttr name d2bHostTools != null + then builtins.getAttr name d2bHostTools + else if builtins.hasAttr name flakePackages + && builtins.getAttr name flakePackages != null + then builtins.getAttr name flakePackages + else throw "d2b Guest package '${name}' is unavailable for ${system}"; + d2bdPackage = + if d2bHostTools != null + && builtins.hasAttr "d2bd" d2bHostTools + && builtins.getAttr "d2bd" d2bHostTools != null + then d2bHostTools.d2bd + else packageFrom "d2bd-guest-static"; + shellRunnerPackage = packageFrom "d2b-guest-shell-runner-static"; + guestUidDefault = + let + digest = builtins.hashString "sha256" "d2b-guest/${name}"; + in + "${builtins.substring 0 8 digest}-${builtins.substring 8 4 digest}-4${builtins.substring 13 3 digest}-8${builtins.substring 17 3 digest}-${builtins.substring 20 12 digest}"; + # These deterministic values keep standalone per-VM evaluation total. The + # enrollment owner must replace them with the Zone-issued identity and + # signed session schema before a Guest can establish a live session. + schemaFingerprintDefault = + "sha256:${builtins.hashString "sha256" "d2b-guest-component-session-v3"}"; + runtimePath = value: + builtins.isString value + && lib.hasPrefix "/" value + && value != "/nix/store" + && !(lib.hasPrefix "/nix/store/" value) + && !(builtins.elem ".." (lib.splitString "/" value)) + && !(lib.hasSuffix "/" value); +in +{ + options.d2b.componentSession = { + enable = lib.mkOption { + type = lib.types.bool; + internal = true; + readOnly = true; + description = "Whether this Guest target agent is enabled."; + }; + + guestConfigPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + internal = true; + readOnly = true; + description = '' + Absolute in-guest path of the operator-editable guest config + working copy that the config-nixos service reads back over the + authenticated ComponentSession. Host-owned, derived from + `d2b.vms..guestConfigFile` independently of any SSH + metadata. When null, the target agent uses its stable absent-config + path and the service fails closed on a read. + ''; + }; + + guestRef = lib.mkOption { + type = lib.types.strMatching "^Guest/[a-z][a-z0-9-]{0,62}$"; + default = "Guest/${name}"; + internal = true; + }; + + guestUid = lib.mkOption { + type = lib.types.strMatching "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"; + default = guestUidDefault; + internal = true; + }; + + zone = lib.mkOption { + type = lib.types.strMatching "^[a-z][a-z0-9-]{0,62}$"; + default = "local"; + internal = true; + }; + + purpose = lib.mkOption { + type = lib.types.enum [ "zone-link" ]; + default = "zone-link"; + internal = true; + }; + + schemaFingerprint = lib.mkOption { + type = lib.types.strMatching "^sha256:[0-9a-f]{64}$"; + default = schemaFingerprintDefault; + internal = true; + }; + + reconnectGeneration = lib.mkOption { + type = lib.types.ints.unsigned; + default = 1; + internal = true; + }; + + providerGeneration = lib.mkOption { + type = lib.types.ints.unsigned; + default = 1; + internal = true; + }; + + controllerGeneration = lib.mkOption { + type = lib.types.ints.unsigned; + default = 1; + internal = true; + }; + + assignmentEpoch = lib.mkOption { + type = lib.types.ints.unsigned; + default = 1; + internal = true; + }; + + brokerSocketPath = lib.mkOption { + type = lib.types.str; + default = "/run/d2b/guest-broker.sock"; + internal = true; + }; + + brokerUid = lib.mkOption { + type = lib.types.ints.unsigned; + default = 997; + internal = true; + }; + + stateDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/d2b/guest-state"; + internal = true; + }; + + bundlePath = lib.mkOption { + type = lib.types.str; + default = "/etc/d2b/guest-bundle.json"; + internal = true; + }; + + localPrivateKeyPath = lib.mkOption { + type = lib.types.str; + default = "/var/lib/d2b/component-session/guest.key"; + internal = true; + }; + + parentPublicKeyPath = lib.mkOption { + type = lib.types.str; + default = "/var/lib/d2b/component-session/parent.pub"; + internal = true; + }; + shell = { + enable = lib.mkOption { + type = lib.types.bool; + internal = true; + readOnly = true; + description = "Host-owned persistent shell policy enable bit."; + }; + + defaultName = lib.mkOption { + type = lib.types.strMatching "^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$"; + internal = true; + readOnly = true; + description = "Host-owned default persistent shell session name."; + }; + + maxSessions = lib.mkOption { + type = lib.types.ints.between 1 256; + internal = true; + readOnly = true; + description = "Host-owned maximum persistent shell sessions per VM."; + }; + + maxAttached = lib.mkOption { + type = lib.types.ints.between 1 64; + internal = true; + readOnly = true; + description = "Host-owned maximum attached persistent shell clients per VM."; + }; + }; + }; + + config = { + assertions = [ + { + assertion = !cfg.shell.enable || cfg.enable; + message = '' + d2b.componentSession.shell.enable requires d2b.componentSession.enable. + Set d2b.vms..guest.componentSession.enable = true on the host-side VM + option before enabling persistent shell policy. + ''; + } + { + assertion = + !cfg.shell.enable + || (config.d2b.sshUser != null && config.d2b.sshUser != "root"); + message = '' + d2b.componentSession.shell.enable requires a configured non-root workload user. + Set d2b.vms..ssh.user to a non-root account so d2b.sshUser is populated + before enabling persistent shell policy. + ''; + } + { + assertion = cfg.shell.maxAttached <= cfg.shell.maxSessions; + message = '' + d2b.componentSession.shell.maxAttached must be less than or equal to + d2b.componentSession.shell.maxSessions. + ''; + } + { + assertion = runtimePath cfg.brokerSocketPath + && runtimePath cfg.stateDir + && runtimePath cfg.bundlePath + && runtimePath cfg.localPrivateKeyPath + && runtimePath cfg.parentPublicKeyPath; + message = '' + d2b.componentSession paths must be absolute runtime + paths outside /nix/store. + ''; + } + ]; + + environment.systemPackages = + [ d2bdPackage ] + ++ lib.optional cfg.shell.enable shellRunnerPackage; + + environment.etc."shpool/config.toml" = lib.mkIf cfg.shell.enable { + text = '' + prompt_prefix = "" + ''; + }; + + systemd.services = { + d2bd-guest = lib.mkIf cfg.enable { + description = "d2b Guest target agent"; + wantedBy = [ "multi-user.target" ]; + wants = [ "d2b-broker-guest.socket" ]; + after = [ "d2b-broker-guest.socket" "network.target" ]; + serviceConfig = { + Type = "simple"; + User = "d2bd"; + Group = "d2bd"; + ExecStart = + let + session = cfg; + configPath = + if cfg.guestConfigPath == null + then "/var/lib/d2b-guest/guest-config.nix" + else cfg.guestConfigPath; + in + "${d2bdPackage}/bin/d2bd guest" + + " --guest-ref ${lib.escapeShellArg session.guestRef}" + + " --guest-uid ${lib.escapeShellArg session.guestUid}" + + " --zone ${lib.escapeShellArg session.zone}" + + " --purpose ${lib.escapeShellArg session.purpose}" + + " --schema-fingerprint ${lib.escapeShellArg session.schemaFingerprint}" + + " --reconnect-generation ${toString session.reconnectGeneration}" + + " --provider-generation ${toString session.providerGeneration}" + + " --controller-generation ${toString session.controllerGeneration}" + + " --assignment-epoch ${toString session.assignmentEpoch}" + + " --broker-socket ${lib.escapeShellArg session.brokerSocketPath}" + + " --broker-uid ${toString session.brokerUid}" + + " --state-dir ${lib.escapeShellArg session.stateDir}" + + " --bundle-path ${lib.escapeShellArg session.bundlePath}" + + " --guest-config-path ${lib.escapeShellArg configPath}" + + " --local-private-key ${lib.escapeShellArg session.localPrivateKeyPath}" + + " --parent-public-key ${lib.escapeShellArg session.parentPublicKeyPath}"; + NoNewPrivileges = true; + CapabilityBoundingSet = [ "" ]; + AmbientCapabilities = [ "" ]; + PrivateTmp = true; + ProtectHome = true; + ProtectClock = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ "AF_UNIX" "AF_VSOCK" ]; + UMask = "0077"; + Restart = "on-failure"; + RestartSec = "2s"; + StandardOutput = "journal"; + StandardError = "journal"; + SyslogIdentifier = "d2bd-guest"; + }; + restartIfChanged = false; + }; + + d2b-shpool-daemon = lib.mkIf (cfg.shell.enable && config.d2b.sshUser != null) { + description = "d2b persistent shell pool daemon"; + serviceConfig = { + Type = "exec"; + User = config.d2b.sshUser; + PAMName = "d2b-shpool-daemon"; + ExecStart = + let + daemonScript = pkgs.writeShellScript "d2b-shpool-daemon-start" '' + set -eu + uid="$(${pkgs.coreutils}/bin/id -u)" + home="$HOME" + export XDG_RUNTIME_DIR="/run/user/$uid" + export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" + exec ${shellRunnerPackage}/bin/d2b-guest-shell-runner daemon \ + --socket "$XDG_RUNTIME_DIR/d2b-shpool.sock" \ + --home "$home" + ''; + in + "${daemonScript}"; + WorkingDirectory = "~"; + KillMode = "control-group"; + Delegate = true; + }; + }; + }; + + security.pam.services.d2b-shpool-daemon = lib.mkIf (cfg.shell.enable && config.d2b.sshUser != null) { + # Do not start a pam_systemd session here: it migrates the daemon out of + # the delegated system service cgroup. Linger keeps /run/user/ + # available while the daemon stays under systemd's service authority. + startSession = false; + setEnvironment = true; + setLoginUid = true; + }; + + users.users = lib.mkIf (cfg.shell.enable && config.d2b.sshUser != null) { + ${config.d2b.sshUser}.linger = true; + }; + + # Guest target state is boot-scoped. The ComponentSession key files are + # enrolled inputs and are never generated from writable state here. + systemd.tmpfiles.rules = + lib.optionals cfg.enable [ + "d ${cfg.stateDir} 0700 d2bd d2bd -" + "d ${builtins.dirOf cfg.localPrivateKeyPath} 0700 d2bd d2bd -" + ] + ; + }; +} diff --git a/nixos-modules/components/graphics.nix b/nixos-modules/components/graphics.nix index 8e477e3cfb..cc8b1c89e3 100644 --- a/nixos-modules/components/graphics.nix +++ b/nixos-modules/components/graphics.nix @@ -496,25 +496,10 @@ in EGL_LOG_LEVEL = "fatal"; }; - # wl-cross-domain-proxy provides the guest-side virtio-gpu - # cross-domain Wayland transport. It is gated on - # crossDomainTrusted so it does not crash-loop when the - # cross-domain crosvm context is absent (crossDomainTrusted=false - # strips the context-type from crosvm's --params, so no - # cross-domain channel exists for the proxy to connect to). - # - # Title rewriting and app-id prefixing are performed on the HOST - # side by d2b-wayland-proxy; the guest proxy does not use - # --tag or any filtering flag. - systemd.user.services.wayland-proxy = lib.mkIf config.d2b.graphics.crossDomainTrusted { - description = "Wayland cross-domain proxy (guest virtio-gpu transport)"; - wantedBy = [ "default.target" ]; - serviceConfig = { - ExecStart = "${wlCrossDomainProxy}/bin/wl-cross-domain-proxy"; - Restart = "on-failure"; - RestartSec = 5; - }; - }; + # Keep the guest-side virtio-gpu transport in the guest closure. Its + # lifecycle is owned by the WaylandSession Guest Process child rather + # than a direct systemd.user service. + environment.systemPackages = lib.optional config.d2b.graphics.crossDomainTrusted wlCrossDomainProxy; # security-r8-audio-8: in-guest foot-autostart removed. # diff --git a/nixos-modules/components/security-key-guest.nix b/nixos-modules/components/security-key-guest.nix index 9a44345bc0..dd26f54849 100644 --- a/nixos-modules/components/security-key-guest.nix +++ b/nixos-modules/components/security-key-guest.nix @@ -91,7 +91,7 @@ in # Not privileged: runs as root only to open /dev/uhid (mode 0600 # root:root on some kernels). If the guest sets uhid to 0660 # root:input, we can drop to the login user with - # User = config.d2b.guestControl.exec.execUser or similar. + # The signed Guest Process keeps ownership of this virtual device. # Keep as root for simplicity; the uhid fd scope is limited to the # virtual device we own. PrivateTmp = true; diff --git a/nixos-modules/components/usbip.nix b/nixos-modules/components/usbip.nix index 4b3df2327a..c24eabedbe 100644 --- a/nixos-modules/components/usbip.nix +++ b/nixos-modules/components/usbip.nix @@ -4,8 +4,7 @@ # This file holds only the GUEST-side wiring: # - vhci_hcd kernel module so `usbip attach` can materialise the # redirected device as /dev/hidraw inside the VM kernel. -# - usbip CLI tools so guestd can perform authenticated guest-side import -# cleanup/attach over guest-control. +# - usbip CLI tools for the signed target-local USBIP Process. # # The HOST-side bits (broker-spawned per-env usbipd/proxy runners, # usbip-host kernel module, udev rules granting kvm-group access to @@ -14,14 +13,11 @@ # being up. # # The hot-plug ceremony is daemon-owned: d2bd drives broker host -# bind/unbind and asks guestd to reconcile guest-side USBIP imports over -# authenticated guest-control. The CLI never SSHes into the guest for USBIP. +# bind/unbind while the target-local USBIP Process owns guest import state. { pkgs, ... }: { boot.kernelModules = [ "vhci_hcd" ]; environment.systemPackages = [ pkgs.linuxPackages.usbip ]; - - d2b.guestControl.usbipPath = "${pkgs.linuxPackages.usbip}/bin/usbip"; } diff --git a/nixos-modules/default.nix b/nixos-modules/default.nix index f7f9f7d342..311354d518 100644 --- a/nixos-modules/default.nix +++ b/nixos-modules/default.nix @@ -57,7 +57,7 @@ in # host-otel-relay-acl.nix retired per ADR 0018. # The OTel host-bridge + per-VM relay ACL contract moved into the # broker pre-spawn pipeline (`SpawnRunner{role: OtelHostBridge}` - # in `packages/d2b-priv-broker/src/runtime.rs`). The retired + # in `packages/d2b-broker/src/runtime.rs`). The retired # module file is kept as a stub for one release for diff # readability; consumers should not import it directly. A future # commit deletes the stub file outright. @@ -74,7 +74,6 @@ in ../packages/d2b-provider-volume-local/nix/store.nix ./manifest.nix ./bundle.nix - ./guest-control-host.nix ./host-json.nix ./processes-json.nix ../packages/d2b-provider-volume-local/nix/storage-json.nix diff --git a/nixos-modules/gateway-vm.nix b/nixos-modules/gateway-vm.nix index 5a1f6fa537..3bb30cc6a9 100644 --- a/nixos-modules/gateway-vm.nix +++ b/nixos-modules/gateway-vm.nix @@ -23,6 +23,7 @@ let if pkg != null then pkg else unavailableHostToolPackage name; d2bdPackage = hostToolPackage "d2bd" "d2bd"; + d2bBrokerPackage = hostToolPackage "d2bBroker" "d2b-broker"; d2bPackage = hostToolPackage "d2b" "d2b"; d2bGatewayRuntimePackage = hostToolPackage "d2bGatewayRuntime" "d2b-gateway-runtime"; @@ -56,6 +57,7 @@ let env = gw.env; index = gw.index; ssh.user = lib.mkDefault "gateway"; + guest.componentSession.enable = true; config = { lib, pkgs, ... }: { networking.hostName = lib.mkDefault gw.vmName; users.groups.d2b = { }; @@ -69,6 +71,10 @@ let isNormalUser = true; extraGroups = [ "wheel" "d2b" ]; }; + d2b.componentSession = { + guestRef = "Guest/${gw.vmName}"; + zone = gw.realm; + }; environment.etc."d2b/daemon-config.json".text = builtins.toJSON { publicSocketPath = "/run/d2b/public.sock"; brokerSocketPath = "/run/d2b/priv.sock"; @@ -164,6 +170,7 @@ let curl d2bPackage d2bdPackage + d2bBrokerPackage d2bGatewayRuntimePackage ]; systemd.tmpfiles.rules = [ @@ -177,20 +184,96 @@ let "d /run/d2b/state 0700 d2bd d2bd -" "d /var/lib/d2b 0750 d2bd d2bd -" "d /var/lib/d2b/daemon-state 0700 d2bd d2bd -" + "d /var/lib/d2b/audit 0750 root d2bd -" + "d /var/lib/d2b/current-bundle 0755 root root -" + "d /run/d2b/broker 0750 root d2bd -" "d /var/cache/d2b 0750 root d2bd -" ] ++ (map (dir: "d ${dir} 0700 d2bd d2bd -") (gatewayStateDirs gw)); + systemd.sockets.d2b-broker = { + description = "d2b child-Zone privileged broker socket"; + wantedBy = [ "sockets.target" ]; + requires = [ "systemd-tmpfiles-setup.service" ]; + after = [ "systemd-tmpfiles-setup.service" ]; + socketConfig = { + ListenSequentialPacket = "/run/d2b/priv.sock"; + SocketUser = "root"; + SocketGroup = "d2bd"; + SocketMode = "0660"; + Accept = false; + FileDescriptorName = "priv.sock"; + }; + }; + systemd.services.d2b-broker = { + description = "d2b child-Zone privileged broker"; + requires = [ "d2b-broker.socket" ]; + after = [ "d2b-broker.socket" "local-fs.target" ]; + environment = { + RUST_LOG = lib.mkDefault "info"; + D2B_BROKER_NFT_BINARY = "${pkgs.nftables}/bin/nft"; + D2B_BROKER_IP_BINARY = "${pkgs.iproute2}/bin/ip"; + D2B_BROKER_USBIP_BINARY = "${pkgs.linuxPackages_latest.usbip}/bin/usbip"; + }; + path = with pkgs; [ nftables acl iproute2 util-linux ]; + serviceConfig = { + Type = "notify"; + NotifyAccess = "main"; + User = "root"; + Group = "d2bd"; + CapabilityBoundingSet = [ + "CAP_CHOWN" + "CAP_DAC_OVERRIDE" + "CAP_DAC_READ_SEARCH" + "CAP_FOWNER" + "CAP_FSETID" + "CAP_IPC_LOCK" + "CAP_KILL" + "CAP_LEASE" + "CAP_MKNOD" + "CAP_NET_ADMIN" + "CAP_NET_RAW" + "CAP_SETFCAP" + "CAP_SETGID" + "CAP_SETPCAP" + "CAP_SETUID" + "CAP_SYS_ADMIN" + "CAP_SYS_RESOURCE" + ]; + AmbientCapabilities = [ "" ]; + NoNewPrivileges = false; + KillMode = "process"; + PrivateTmp = true; + ProtectHome = false; + ProtectClock = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ "AF_UNIX" "AF_VSOCK" "AF_INET" "AF_INET6" ]; + UMask = "0027"; + ExecStart = + "${d2bBrokerPackage}/bin/d2b-broker host " + + "--authority-id child-zone-${name} " + + "--audit-dir /var/lib/d2b/audit " + + "--bundle-path /etc/d2b/bundle.json " + + "--state-dir /var/lib/d2b " + + "--d2bd-uid 997 " + + "--d2bd-gid 997"; + Restart = "on-failure"; + RestartSec = "2s"; + StandardOutput = "journal"; + StandardError = "journal"; + SyslogIdentifier = "d2b-broker-child-zone"; + }; + }; systemd.services.d2bd = { description = "d2b realm gateway daemon"; wantedBy = [ "multi-user.target" ]; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; + after = [ "network-online.target" "d2b-broker.socket" ]; + wants = [ "network-online.target" "d2b-broker.socket" ]; restartIfChanged = false; serviceConfig = { Type = "simple"; User = "d2bd"; Group = "d2bd"; SupplementaryGroups = [ "d2b" ]; - ExecStart = "${d2bdPackage}/bin/d2bd serve --config /etc/d2b/daemon-config.json"; + ExecStart = "${d2bdPackage}/bin/d2bd host --config /etc/d2b/daemon-config.json"; Restart = "on-failure"; RestartSec = "2s"; NoNewPrivileges = true; diff --git a/nixos-modules/generated/provider-catalog-shape.nix b/nixos-modules/generated/provider-catalog-shape.nix index e9a01aa0bb..5c09fd5587 100644 --- a/nixos-modules/generated/provider-catalog-shape.nix +++ b/nixos-modules/generated/provider-catalog-shape.nix @@ -134,4 +134,19 @@ "runtime-marketplace" "version-range-solving" ]; + + # Signed placement-contract fields carried by the Provider + # manifest and consumed by Core admission. + placementContractFields = [ + "instanceScope" + "supportedTargetKinds" + "targetCapabilities" + "placementAnchor" + ]; + + # Signed shared daemon and broker artifact fields. + runtimeContractFields = [ + "d2bdDigest" + "brokerDigest" + ]; } diff --git a/nixos-modules/guest-broker.nix b/nixos-modules/guest-broker.nix new file mode 100644 index 0000000000..ee771180c4 --- /dev/null +++ b/nixos-modules/guest-broker.nix @@ -0,0 +1,112 @@ +# d2b-broker guest profile +# +# Every workload Guest gets a separate socket-activated broker instance. The +# executable is the same host-tool derivation used by the Host profile; only +# the process-start profile and authority roots differ. +{ config, lib, pkgs, name, d2bHostTools, d2bUsePrebuiltHostTools ? false, ... }: + +let + prebuilt = + if d2bUsePrebuiltHostTools + then import ./prebuilt-packages.nix { inherit pkgs lib; } + else { }; + brokerSourcePackage = d2bHostTools.broker; + brokerPackage = + if prebuilt ? "selectPackage" + then prebuilt.selectPackage "d2b-broker" brokerSourcePackage + else brokerSourcePackage; + brokerSocket = "/run/d2b/guest-broker.sock"; + brokerState = "/var/lib/d2b/guest-broker"; + brokerAudit = "/var/lib/d2b/guest-audit"; + brokerBundle = "/etc/d2b/guest-bundle.json"; + brokerUid = config.users.users.d2bd.uid or 997; + brokerGid = config.users.groups.d2bd.gid or 997; +in +{ + users.groups.d2bd = { + gid = lib.mkDefault 997; + }; + users.users.d2bd = { + isSystemUser = true; + group = "d2bd"; + uid = lib.mkDefault 997; + }; + + environment.systemPackages = [ brokerPackage ]; + + systemd.tmpfiles.rules = [ + "d /run/d2b 0750 root d2bd -" + "d ${brokerState} 0700 root d2bd -" + "d ${brokerAudit} 0750 root d2bd -" + ]; + + systemd.sockets.d2b-broker-guest = { + description = "d2b Guest privileged broker socket"; + wantedBy = [ "sockets.target" ]; + requires = [ "systemd-tmpfiles-setup.service" ]; + after = [ "systemd-tmpfiles-setup.service" ]; + socketConfig = { + ListenSequentialPacket = brokerSocket; + SocketUser = "root"; + SocketGroup = "d2bd"; + SocketMode = "0660"; + Accept = false; + FileDescriptorName = "priv.sock"; + }; + }; + + systemd.services.d2b-broker-guest = { + description = "d2b Guest privileged broker"; + requires = [ + "d2b-broker-guest.socket" + "systemd-tmpfiles-setup.service" + ]; + after = [ + "d2b-broker-guest.socket" + "systemd-tmpfiles-setup.service" + "local-fs.target" + ]; + environment = { + RUST_LOG = lib.mkDefault "info"; + }; + serviceConfig = { + Type = "notify"; + NotifyAccess = "main"; + User = "root"; + Group = "d2bd"; + CapabilityBoundingSet = [ + "CAP_DAC_OVERRIDE" + "CAP_DAC_READ_SEARCH" + "CAP_FOWNER" + "CAP_KILL" + "CAP_SETGID" + "CAP_SETUID" + "CAP_SYS_ADMIN" + "CAP_SYS_RESOURCE" + ]; + AmbientCapabilities = [ "" ]; + NoNewPrivileges = false; + KillMode = "process"; + PrivateTmp = true; + ProtectHome = true; + ProtectClock = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ "AF_UNIX" "AF_VSOCK" ]; + SystemCallArchitectures = "native"; + UMask = "0027"; + ExecStart = + "${brokerPackage}/bin/d2b-broker guest " + + "--authority-id guest-${name} " + + "--audit-dir ${brokerAudit} " + + "--bundle-path ${brokerBundle} " + + "--state-dir ${brokerState} " + + "--d2bd-uid ${toString brokerUid} " + + "--d2bd-gid ${toString brokerGid}"; + Restart = "on-failure"; + RestartSec = "2s"; + StandardOutput = "journal"; + StandardError = "journal"; + SyslogIdentifier = "d2b-broker-guest"; + }; + }; +} diff --git a/nixos-modules/guest-control-host.nix b/nixos-modules/guest-control-host.nix deleted file mode 100644 index 3b1eb99851..0000000000 --- a/nixos-modules/guest-control-host.nix +++ /dev/null @@ -1,139 +0,0 @@ -{ config, pkgs, lib, ... }: - -let - cfg = config.d2b; - d2bLib = import ./lib.nix { inherit lib; }; - enabledGuestControlVms = - lib.filterAttrs (_: vm: vm.enable && vm.guest.control.enable) cfg.vms; - usernamePattern = "^[a-z][a-z0-9_-]{0,31}$"; - usernameValid = user: builtins.match usernamePattern user != null; - tokenSpecs = lib.mapAttrsToList (name: vm: { - inherit name; - source = vm.guest.control.auth.tokenFile; - target = "${cfg.site.stateDir}/guest-control-${name}/token"; - readerGid = d2bLib.stablePrincipalId "d2b-${name}-gctlfs"; - }) enabledGuestControlVms; - tokenMaterializer = ./guest-control-token-materialize.py; -in -{ - assertions = lib.mapAttrsToList (name: vm: { - assertion = vm.guest.control.enable || vm.guest.control.auth.tokenFile == null; - message = '' - d2b.vms.${name}.guest.control.auth.tokenFile is set, but - d2b.vms.${name}.guest.control.enable is false. Enable guest-control - token delivery or remove the tokenFile setting. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - vm.guest.control.auth.tokenFile == null - || ( - lib.hasPrefix "/" vm.guest.control.auth.tokenFile - && vm.guest.control.auth.tokenFile != "/nix/store" - && !(lib.hasPrefix "/nix/store/" vm.guest.control.auth.tokenFile) - ); - message = '' - d2b.vms.${name}.guest.control.auth.tokenFile must be an absolute - runtime path outside /nix/store. Do not use Nix path literals or - flake-relative secret files; use a runtime secret path such as - /run/secrets/d2b/${name}/guest-control-token. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.guest.exec.enable - || vm.guest.control.enable; - message = '' - d2b.vms.${name}.guest.exec.enable requires - d2b.vms.${name}.guest.control.enable because exec policy is enforced - by the guest-control plane. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.enable - || !vm.usbip.yubikey - || vm.guest.control.enable; - message = '' - d2b.vms.${name}.usbip.yubikey requires - d2b.vms.${name}.guest.control.enable because guest-side USBIP - attach/detach is owned by guestd over the authenticated guest-control - plane. There is no SSH fallback. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.guest.exec.enable - || vm.ssh.user != null; - message = '' - d2b.vms.${name}.guest.exec.enable is true, but no workload user is - configured. Guest exec always runs the command as the VM's workload user - (never root); set d2b.vms.${name}.ssh.user to the in-guest user exec - should run as. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.guest.exec.enable - || vm.ssh.user == null - || (usernameValid vm.ssh.user && vm.ssh.user != "root"); - message = '' - d2b.vms.${name}.ssh.user (the guest exec workload user) must match - ${usernamePattern} and must not be root. Guest exec never runs as root; - users elevate with sudo inside the session. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.guest.shell.enable - || vm.guest.control.enable; - message = '' - d2b.vms.${name}.guest.shell.enable requires - d2b.vms.${name}.guest.control.enable because persistent shell policy - is enforced by the authenticated guest-control plane. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.guest.shell.enable - || vm.guest.exec.enable; - message = '' - d2b.vms.${name}.guest.shell.enable requires - d2b.vms.${name}.guest.exec.enable because persistent shells reuse the - guest-control exec terminal substrate and workload-user policy. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.guest.shell.enable - || vm.ssh.user != null; - message = '' - d2b.vms.${name}.guest.shell.enable is true, but no workload user is - configured. Persistent shells run as the VM's workload user (never root); - set d2b.vms.${name}.ssh.user. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = - !vm.guest.shell.enable - || vm.ssh.user == null - || (usernameValid vm.ssh.user && vm.ssh.user != "root"); - message = '' - d2b.vms.${name}.ssh.user (the persistent shell workload user) must - match ${usernamePattern} and must not be root. - ''; - }) cfg.vms - ++ lib.mapAttrsToList (name: vm: { - assertion = vm.guest.shell.maxAttached <= vm.guest.shell.maxSessions; - message = '' - d2b.vms.${name}.guest.shell.maxAttached must be less than or equal to - guest.shell.maxSessions. - ''; - }) cfg.vms; - - system.activationScripts.d2bGuestControlTokens = - lib.stringAfter [ "users" ] '' - ${pkgs.coreutils}/bin/printf '%s\n' ${lib.escapeShellArg (builtins.toJSON tokenSpecs)} \ - | ${pkgs.python3}/bin/python3 ${tokenMaterializer} - - ''; -} diff --git a/nixos-modules/guest-control-token-materialize.py b/nixos-modules/guest-control-token-materialize.py deleted file mode 100644 index 151bcf38ee..0000000000 --- a/nixos-modules/guest-control-token-materialize.py +++ /dev/null @@ -1,223 +0,0 @@ -import json -import os -import secrets -import stat -import sys - -if sys.argv[1] == "-": - specs = json.load(sys.stdin) -else: - specs = json.load(open(sys.argv[1], encoding="utf-8")) -current_vm = "" - - -def fail(kind): - print(f"d2b guest-control token {current_vm}: {kind}", file=sys.stderr) - sys.exit(1) - - -def reject_symlink_components(path, *, require_existing_file, check_parent_permissions): - if not os.path.isabs(path): - fail("path-not-absolute") - current = "/" - parts = [part for part in path.split("/") if part] - check_parts = parts if require_existing_file else parts[:-1] - for part in check_parts: - current = os.path.join(current, part) - try: - st = os.lstat(current) - except FileNotFoundError: - if require_existing_file: - fail("path-component-missing") - break - if stat.S_ISLNK(st.st_mode): - fail("path-component-symlink") - if check_parent_permissions and stat.S_ISDIR(st.st_mode) and st.st_uid != 0: - fail("path-component-not-root-owned") - if check_parent_permissions and stat.S_ISDIR(st.st_mode) and ( - stat.S_IMODE(st.st_mode) & 0o022 - ): - fail("path-component-group-or-world-writable") - - -def validate_materialized(path, reader_gid): - reject_symlink_components(path, require_existing_file=True, check_parent_permissions=False) - try: - fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC) - except OSError: - fail("materialized-open-failed") - try: - st = os.fstat(fd) - if not stat.S_ISREG(st.st_mode): - fail("materialized-not-regular") - os.fchown(fd, 0, reader_gid) - os.fchmod(fd, 0o440) - finally: - os.close(fd) - - -def copy_operator_token(source, target, reader_gid): - if not os.path.isabs(source): - fail("source-not-absolute") - if source == "/nix/store" or source.startswith("/nix/store/"): - fail("source-in-nix-store") - reject_symlink_components(source, require_existing_file=True, check_parent_permissions=True) - try: - src_fd = os.open(source, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC) - except OSError: - fail("source-open-failed") - try: - st = os.fstat(src_fd) - if not stat.S_ISREG(st.st_mode): - fail("source-not-regular") - if st.st_uid != 0: - fail("source-not-root-owned") - if stat.S_IMODE(st.st_mode) & 0o077: - fail("source-group-or-world-permissions") - write_fd_to_target(src_fd, target, reader_gid) - write_source_marker(target, "operator") - finally: - os.close(src_fd) - - -def write_fd_to_target(src_fd, target, reader_gid): - directory = os.path.dirname(target) - prepare_target_directory(directory, reader_gid) - tmp = os.path.join(directory, f".token.tmp.{os.getpid()}") - try: - dst_fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC, 0o400) - try: - while True: - chunk = os.read(src_fd, 65536) - if not chunk: - break - os.write(dst_fd, chunk) - os.fchown(dst_fd, 0, reader_gid) - os.fchmod(dst_fd, 0o440) - os.fsync(dst_fd) - finally: - os.close(dst_fd) - os.rename(tmp, target) - finally: - try: - os.unlink(tmp) - except FileNotFoundError: - pass - - -def generate_token(target, reader_gid): - if os.path.exists(target) and read_source_marker(target) == "generated": - validate_materialized(target, reader_gid) - return - directory = os.path.dirname(target) - prepare_target_directory(directory, reader_gid) - tmp = os.path.join(directory, f".token.tmp.{os.getpid()}") - token = (secrets.token_urlsafe(48) + "\n").encode("ascii") - try: - fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC, 0o400) - try: - os.write(fd, token) - os.fchown(fd, 0, reader_gid) - os.fchmod(fd, 0o440) - os.fsync(fd) - finally: - os.close(fd) - os.rename(tmp, target) - finally: - try: - os.unlink(tmp) - except FileNotFoundError: - pass - validate_materialized(target, reader_gid) - write_source_marker(target, "generated") - - -def marker_path(target): - return os.path.join(os.path.dirname(target), "token.source") - - -def read_source_marker(target): - path = marker_path(target) - try: - fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC) - except OSError: - return None - try: - data = os.read(fd, 64).decode("ascii", errors="ignore").strip() - if data in ("generated", "operator"): - return data - return None - finally: - os.close(fd) - - -def write_source_marker(target, value): - directory = os.path.dirname(target) - tmp = os.path.join(directory, f".token.source.tmp.{os.getpid()}") - path = marker_path(target) - try: - fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC, 0o600) - try: - os.write(fd, (value + "\n").encode("ascii")) - os.fchown(fd, 0, 0) - os.fchmod(fd, 0o600) - os.fsync(fd) - finally: - os.close(fd) - os.rename(tmp, path) - finally: - try: - os.unlink(tmp) - except FileNotFoundError: - pass - - -def prepare_target_directory(directory, reader_gid): - reject_symlink_components(directory, require_existing_file=False, check_parent_permissions=False) - parent = os.path.dirname(directory) - created_parent = False - try: - os.mkdir(parent, mode=0o750) - created_parent = True - except FileExistsError: - pass - try: - parent_fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC) - except OSError: - fail("target-parent-open-failed") - try: - st = os.fstat(parent_fd) - if not stat.S_ISDIR(st.st_mode): - fail("target-parent-not-directory") - if created_parent: - os.fchown(parent_fd, 0, 0) - os.fchmod(parent_fd, 0o750) - finally: - os.close(parent_fd) - try: - os.mkdir(directory, mode=0o750) - except FileExistsError: - pass - try: - fd = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC) - except OSError: - fail("target-directory-open-failed") - try: - st = os.fstat(fd) - if not stat.S_ISDIR(st.st_mode): - fail("target-directory-not-directory") - os.fchown(fd, 0, reader_gid) - os.fchmod(fd, 0o750) - finally: - os.close(fd) - - -for spec in specs: - current_vm = spec["name"] - target = spec["target"] - reader_gid = int(spec.get("readerGid", 0)) - source = spec.get("source") - if source is None: - generate_token(target, reader_gid) - else: - copy_operator_token(source, target, reader_gid) diff --git a/nixos-modules/guest-control.nix b/nixos-modules/guest-control.nix deleted file mode 100644 index f11b36b948..0000000000 --- a/nixos-modules/guest-control.nix +++ /dev/null @@ -1,367 +0,0 @@ -{ config, lib, name, d2bInputs, pkgs, ... }: - -let - cfg = config.d2b.guestControl; - guestPackages = d2bInputs.self.packages.${pkgs.stdenv.hostPlatform.system}; - usernamePattern = "^[a-z][a-z0-9_-]{0,31}$"; - usernameValid = user: builtins.match usernamePattern user != null; - userExists = user: - let - userCfg = config.users.users.${user}; - in - builtins.hasAttr user config.users.users - && ((userCfg.isNormalUser or false) || (userCfg.isSystemUser or false)); - # Exec runtime is wired whenever exec is enabled for a workload user. - # The detached runtime paths + substrate (parent dir + slice) are part - # of a both-or-neither bundle with the attached exec runtime; guestd - # decides at runtime whether to actually serve detached. - execRuntimeEnabled = cfg.exec.enable && cfg.exec.execUser != null; -in -{ - options.d2b.guestControl = { - enable = lib.mkOption { - type = lib.types.bool; - internal = true; - readOnly = true; - description = "Whether d2b's guest-control credential surface is wired in this guest."; - }; - - guestConfigPath = lib.mkOption { - type = lib.types.nullOr lib.types.str; - internal = true; - readOnly = true; - description = '' - Absolute in-guest path of the operator-editable guest config - working copy that `d2b config sync` reads back over the - authenticated guest-control channel. Host-owned, derived from - `d2b.vms..guestConfigFile` independently of any SSH - metadata. When non-null, guestd advertises the `ReadGuestFile` - capability and serves a bounded read of exactly this path; when - null there is nothing to sync and the capability stays absent - (config sync fails closed). - ''; - }; - - usbipPath = lib.mkOption { - type = lib.types.nullOr lib.types.str; - internal = true; - readOnly = true; - description = '' - Absolute in-guest path to the USBIP CLI. Non-null only for guests - with the USBIP component enabled; guestd then advertises the - `UsbipImport` capability and owns guest-side USBIP attach/detach. - ''; - }; - - wpctlPath = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - internal = true; - description = '' - Absolute in-guest path to the `wpctl` binary (from the - `wireplumber` package). Non-null only for guests with the audio - component enabled. When non-null and the workload user is - configured, guestd advertises the `AudioStatus` and `AudioSet` - capabilities and serves PipeWire queries targeting the workload - user's session via argv-only wpctl subprocesses. - ''; - }; - - exec = { - enable = lib.mkOption { - type = lib.types.bool; - internal = true; - readOnly = true; - description = "Host-owned guest exec policy enable bit."; - }; - - execUser = lib.mkOption { - type = lib.types.nullOr lib.types.str; - internal = true; - readOnly = true; - description = '' - Host-fixed workload user every guest exec runs as (never root). - Derived from the per-VM workload user (`ssh.user`). When non-null, - guestd is launched with `--exec-user ` and every exec runs the - requested command as this user in a real PAM login session - (`systemd-run --property=PAMName=login --uid=`). - ''; - }; - - detachedMaxRuntimeSec = lib.mkOption { - type = lib.types.ints.unsigned; - internal = true; - readOnly = true; - description = '' - Host-owned default runtime ceiling (seconds) for detached execs. - 0 means no ceiling (indefinite runtime). - ''; - }; - - interactiveMaxRuntimeSec = lib.mkOption { - type = lib.types.ints.unsigned; - internal = true; - readOnly = true; - description = '' - Host-owned default runtime ceiling (seconds) for interactive (TTY) - execs. 0 means no ceiling (indefinite, connection-owned runtime). - ''; - }; - }; - - shell = { - enable = lib.mkOption { - type = lib.types.bool; - internal = true; - readOnly = true; - description = "Host-owned persistent shell policy enable bit."; - }; - - defaultName = lib.mkOption { - type = lib.types.strMatching "^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$"; - internal = true; - readOnly = true; - description = "Host-owned default persistent shell session name."; - }; - - maxSessions = lib.mkOption { - type = lib.types.ints.between 1 256; - internal = true; - readOnly = true; - description = "Host-owned maximum persistent shell sessions per VM."; - }; - - maxAttached = lib.mkOption { - type = lib.types.ints.between 1 64; - internal = true; - readOnly = true; - description = "Host-owned maximum attached persistent shell clients per VM."; - }; - }; - }; - - config = { - assertions = [ - { - assertion = - !cfg.exec.enable - || cfg.enable; - message = '' - d2b.guestControl.exec.enable requires d2b.guestControl.enable. - Set d2b.vms..guest.control.enable = true on the host-side VM - option before enabling guest exec policy. - ''; - } - { - # Exec runs as the workload user; a workload user MUST be configured. - assertion = !cfg.exec.enable || cfg.exec.execUser != null; - message = '' - d2b.vms..guest.exec.enable is true, but no workload user is - configured. Guest exec always runs as the VM's workload user; set - d2b.vms..ssh.user to the in-guest user exec should run as. - ''; - } - { - # The workload user must be a valid, non-root account. - assertion = - !cfg.exec.enable - || cfg.exec.execUser == null - || (usernameValid cfg.exec.execUser && cfg.exec.execUser != "root"); - message = '' - d2b.vms..ssh.user (used as the guest exec workload user) must - match ${usernamePattern} and must not be root. Guest exec never runs - as root; users elevate with sudo inside the session. - ''; - } - { - # The workload user must exist in the guest so login/PAM can resolve it. - assertion = - !cfg.exec.enable - || cfg.exec.execUser == null - || userExists cfg.exec.execUser; - message = '' - d2b.vms..ssh.user (the guest exec workload user) is not - declared as a normal or system user inside the guest. Declare it (or - enable the desktop/home-manager user) before enabling guest exec. - ''; - } - { - # The workload user must not resolve to UID 0 (root) even under a - # non-root name. The name check above rejects only the literal "root", - # but the never-root contract is about the effective UID, so an explicit - # `uid = 0` alias must also be rejected. The guestd daemon additionally - # resolves the effective UID from the guest passwd DB at runtime and - # refuses 0; this is the eval-time half of that defense. - assertion = - !cfg.exec.enable - || cfg.exec.execUser == null - || !(builtins.hasAttr cfg.exec.execUser config.users.users) - || (config.users.users.${cfg.exec.execUser}.uid or null) != 0; - message = '' - d2b.vms..ssh.user (the guest exec workload user) is configured - with uid = 0. Guest exec never runs as root; assign the workload user - a non-zero uid. - ''; - } - { - assertion = !cfg.shell.enable || cfg.enable; - message = '' - d2b.guestControl.shell.enable requires d2b.guestControl.enable. - Set d2b.vms..guest.control.enable = true on the host-side VM - option before enabling persistent shell policy. - ''; - } - { - assertion = !cfg.shell.enable || cfg.exec.enable; - message = '' - d2b.guestControl.shell.enable requires d2b.guestControl.exec.enable - because persistent shells reuse the guest-control exec terminal substrate. - ''; - } - { - assertion = cfg.shell.maxAttached <= cfg.shell.maxSessions; - message = '' - d2b.guestControl.shell.maxAttached must be less than or equal to - d2b.guestControl.shell.maxSessions. - ''; - } - ]; - - environment.systemPackages = [ - guestPackages.d2b-guestd-static - guestPackages.d2b-exec-runner-static - ]; - - environment.etc."shpool/config.toml" = lib.mkIf cfg.shell.enable { - text = '' - prompt_prefix = "" - ''; - }; - - systemd.services = { - d2b-guestd = lib.mkIf cfg.enable { - description = "d2b guest control daemon"; - wantedBy = [ "multi-user.target" ]; - unitConfig.RequiresMountsFor = [ "/run/d2b-guest-control-host" ]; - serviceConfig = { - Type = "exec"; - ExecStart = - let - execEnabledUser = cfg.exec.enable && cfg.exec.execUser != null; - execFlags = - lib.optionalString cfg.exec.enable " --exec-enable" - + lib.optionalString execEnabledUser - " --exec-user ${lib.escapeShellArg cfg.exec.execUser}" - + lib.optionalString execEnabledUser - " --interactive-max-runtime-sec ${toString cfg.exec.interactiveMaxRuntimeSec}"; - # The reachable exec paths (interactive PTY + non-interactive pipe) - # run the command as the workload user in a PAM login session via - # systemd-run, using the exec-runner PTY helper, so both binary - # paths are wired whenever exec is enabled - not only for detached. - # guestd treats the two paths as a both-or-neither bundle. - execRuntimeFlags = - lib.optionalString execEnabledUser ( - " --systemd-run-path ${pkgs.systemd}/bin/systemd-run" - + " --exec-runner-path ${guestPackages.d2b-exec-runner-static}/bin/d2b-exec-runner" - + " --detached-max-runtime-sec ${toString cfg.exec.detachedMaxRuntimeSec}" - ); - # config sync read surface: advertised iff the host - # declared a guestConfigFile (path threaded independently - # of ssh.user). guestd gates the ReadGuestFile capability - # on this flag being present. - configFlags = - lib.optionalString (cfg.guestConfigPath != null) - " --guest-config-path ${lib.escapeShellArg cfg.guestConfigPath}"; - usbipFlags = - lib.optionalString (cfg.usbipPath != null) - " --usbip-path ${lib.escapeShellArg cfg.usbipPath}"; - audioFlags = - lib.optionalString (cfg.wpctlPath != null) - " --wpctl-path ${lib.escapeShellArg cfg.wpctlPath}"; - activationFlags = - " --activation-systemd-run-path ${pkgs.systemd}/bin/systemd-run" - + " --activation-systemctl-path ${pkgs.systemd}/bin/systemctl"; - shellFlags = - lib.optionalString cfg.shell.enable ( - " --shell-enable" - + " --shell-default-name ${lib.escapeShellArg cfg.shell.defaultName}" - + " --shell-max-sessions ${toString cfg.shell.maxSessions}" - + " --shell-max-attached ${toString cfg.shell.maxAttached}" - + lib.optionalString execEnabledUser - " --shell-runner-path ${guestPackages.d2b-guest-shell-runner-static}/bin/d2b-guest-shell-runner" - + lib.optionalString execEnabledUser - " --shell-systemctl-path ${pkgs.systemd}/bin/systemctl" - ); - in - "${guestPackages.d2b-guestd-static}/bin/d2b-guestd --serve --vm-id ${lib.escapeShellArg name}${execFlags}${execRuntimeFlags}${configFlags}${usbipFlags}${audioFlags}${activationFlags}${shellFlags}"; - LoadCredential = [ - "guest_control_token:/run/d2b-guest-control-host/token" - ]; - }; - restartIfChanged = false; - }; - - d2b-shpool-daemon = lib.mkIf (cfg.shell.enable && cfg.exec.execUser != null) { - description = "d2b persistent shell pool daemon"; - serviceConfig = { - Type = "exec"; - User = cfg.exec.execUser; - PAMName = "d2b-shpool-daemon"; - ExecStart = - let - daemonScript = pkgs.writeShellScript "d2b-shpool-daemon-start" '' - set -eu - uid="$(${pkgs.coreutils}/bin/id -u)" - home="$HOME" - export XDG_RUNTIME_DIR="/run/user/$uid" - export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" - exec ${guestPackages.d2b-guest-shell-runner-static}/bin/d2b-guest-shell-runner daemon \ - --socket "$XDG_RUNTIME_DIR/d2b-shpool.sock" \ - --home "$home" - ''; - in - "${daemonScript}"; - WorkingDirectory = "~"; - KillMode = "control-group"; - Delegate = true; - }; - }; - }; - - security.pam.services.d2b-shpool-daemon = lib.mkIf (cfg.shell.enable && cfg.exec.execUser != null) { - # Do not start a pam_systemd session here: it migrates the daemon out of - # the delegated system service cgroup. Linger keeps /run/user/ - # available while the daemon stays under systemd's service authority. - startSession = false; - setEnvironment = true; - setLoginUid = true; - }; - - users.users = lib.mkIf (cfg.shell.enable && cfg.exec.execUser != null) { - ${cfg.exec.execUser}.linger = true; - }; - - # Detached exec runtime substrate (parent dir + slice), declared as - # part of the both-or-neither exec runtime bundle whenever exec is - # enabled for a workload user. The parent dir is root-owned, 0700, - # boot-scoped (D = clear at boot, NOT on every guestd restart) so - # detached slot state survives a guestd restart for re-adoption. Do - # NOT make this guestd's RuntimeDirectory without - # RuntimeDirectoryPreserve, else a restart wipes adoptable state. - systemd.tmpfiles.rules = - lib.optionals cfg.enable [ - "d /run/d2b-guestd 0700 root root -" - "d /run/d2b-guestd/activations 0700 root root -" - ] - ++ lib.optionals execRuntimeEnabled [ - "D /run/d2b-exec 0700 root root -" - ]; - - # Guest-internal slice that scopes every per-exec transient slot unit - # (d2b-exec-NN.service). Slot-keyed unit names bound metadata - # cardinality to <=32 stable values that carry no exec id. - systemd.slices."d2b-exec" = lib.mkIf execRuntimeEnabled { - description = "d2b detached guest exec slice"; - }; - }; -} diff --git a/nixos-modules/host-activation.nix b/nixos-modules/host-activation.nix index 0bbef9ddba..5d014ab0e9 100644 --- a/nixos-modules/host-activation.nix +++ b/nixos-modules/host-activation.nix @@ -110,13 +110,10 @@ let (name: vm: let runnerPrincipal = stablePrincipal "d2b-${name}-runner"; - gctlfsPrincipal = stablePrincipal "d2b-${name}-gctlfs"; in lib.concatLists [ (runtimeAclUser "/run/d2b" runnerPrincipal "--x") - (runtimeAclUser "/run/d2b" gctlfsPrincipal "--x") (runtimeAclUser "/run/d2b/vms" runnerPrincipal "--x") - (runtimeAclUser "/run/d2b/vms" gctlfsPrincipal "--x") (lib.optionals vm.tpm.enable (runtimeAclUser "/run/d2b" "d2b-${name}-swtpm" "--x")) (lib.optionals vm.tpm.enable (runtimeAclUser "/run/d2b/vms" "d2b-${name}-swtpm" "--x")) (lib.optionals vm.audio.enable (runtimeAclUser "/run/d2b" "d2b-${name}-snd" "--x")) @@ -149,17 +146,14 @@ let (name: vm: let vmRunDir = "/run/d2b/vms/${name}"; - guestControlRunDir = "${vmRunDir}/guest-control"; gpuRunDir = "/run/d2b-gpu/${name}"; videoRunDir = "/run/d2b-video/${name}"; wlproxyRunDir = "/run/d2b-wlproxy/${name}"; runnerPrincipal = stablePrincipal "d2b-${name}-runner"; - gctlfsPrincipal = stablePrincipal "d2b-${name}-gctlfs"; in lib.concatLists [ (tmpfilesDir "/var/lib/d2b/vms/${name}" "3770" "d2bd" "users") (runtimeLeafDir vmRunDir "1770" "d2bd" "d2b") - (runtimeLeafDir guestControlRunDir "0770" "d2bd" "d2b") (runtimeLeafDir gpuRunDir "0770" "d2bd" "d2b") (runtimeLeafDir videoRunDir "0770" "d2bd" "d2b") (runtimeLeafDir wlproxyRunDir "0770" "d2bd" "d2b") @@ -168,13 +162,7 @@ let (tmpfilesDir "/var/lib/d2b/vms/${name}/store-view/meta" "0755" "d2bd" "users") (runtimeAclMask vmRunDir) (runtimeAclUser vmRunDir runnerPrincipal "rwx") - (runtimeAclUser vmRunDir gctlfsPrincipal "--x") (runtimeDefaultAclUser vmRunDir runnerPrincipal "rwx") - (runtimeAclMask guestControlRunDir) - (runtimeAclUser guestControlRunDir runnerPrincipal "--x") - (runtimeAclUser guestControlRunDir gctlfsPrincipal "rwx") - (runtimeDefaultAclUser guestControlRunDir runnerPrincipal "rwx") - (runtimeDefaultAclUser guestControlRunDir gctlfsPrincipal "rwx") (lib.optionals vm.tpm.enable (tmpfilesAcl "/var/lib/d2b/vms/${name}" "u:d2b-${name}-swtpm:--x")) (lib.optionals vm.tpm.enable (runtimeAclUser vmRunDir "d2b-${name}-swtpm" "rwx")) (lib.optionals vm.graphics.enable (tmpfilesAcl "/var/lib/d2b/vms/${name}" "u:d2b-${name}-gpu:rwx")) @@ -228,8 +216,7 @@ in # Shared runtime parents stay root-owned so broker path-safety checks can # create or reconcile per-VM children without trusting a daemon-writable # parent. The per-VM leaves below remain d2bd:d2b for daemon-owned - # sockets and guest-control artifacts. - (tmpfilesDir "/run/d2b-heavy-gates" "0755" "root" "root") + # sockets and per-VM runtime artifacts. (tmpfilesDir "/run/d2b/vms" "0750" "root" "d2b") (tmpfilesDir "/run/d2b/otel" "0750" "d2bd" "d2b") (tmpfilesDir "/run/d2b-gpu" "0750" "root" "d2b") @@ -249,61 +236,6 @@ in (runtimeAclMask "/run/d2b") ]; - # Numeric UIDs for normal users may be allocated only when the users - # activation step runs, so they cannot be embedded in static tmpfiles rules. - # Tmpfiles owns the fixed root; this activation step provisions slots for - # lifecycle users that NSS can resolve at activation time. Network-backed - # users may not resolve until networking and their identity provider are - # available, so they are deferred to the explicit post-login - # `make heavy-gate-provision` path. The gate remains fail-closed while their - # per-UID directory is absent. Existing regular slot inodes are repaired in - # place so a switch cannot replace an inode carrying a lock. - system.activationScripts.d2bHeavyGateProvision = lib.stringAfter [ "users" ] '' - set -eu - gate_root=/run/d2b-heavy-gates - - if [ -L "$gate_root" ] || { [ -e "$gate_root" ] && [ ! -d "$gate_root" ]; }; then - echo "d2b heavy-gate provisioning: refusing an unsafe runtime root" >&2 - exit 1 - fi - ${pkgs.coreutils}/bin/install -d -m 0755 -o root -g root "$gate_root" - - for user in ${lib.escapeShellArgs cfg.site.launcherUsers}; do - if ! passwd_record="$(${pkgs.getent}/bin/getent passwd "$user")"; then - echo "d2b heavy-gate provisioning: configured lifecycle user is unavailable during activation; skipping; run make heavy-gate-provision as that user after login" >&2 - continue - fi - uid="$(${pkgs.coreutils}/bin/printf '%s\n' "$passwd_record" | ${pkgs.coreutils}/bin/cut -d: -f3)" - case "$uid" in - ""|*[!0-9]*) - echo "d2b heavy-gate provisioning: lifecycle user has no usable uid during activation; skipping; run make heavy-gate-provision as that user after login" >&2 - continue - ;; - esac - - uid_dir="$gate_root/uid-$uid" - if [ -L "$uid_dir" ] || { [ -e "$uid_dir" ] && [ ! -d "$uid_dir" ]; }; then - echo "d2b heavy-gate provisioning: refusing an unsafe per-user directory" >&2 - exit 1 - fi - ${pkgs.coreutils}/bin/install -d -m 0755 -o root -g root "$uid_dir" - - for index in 0 1; do - slot="$uid_dir/slot-$index" - if [ -L "$slot" ] || { [ -e "$slot" ] && [ ! -f "$slot" ]; }; then - echo "d2b heavy-gate provisioning: refusing an unsafe slot file" >&2 - exit 1 - fi - if [ ! -e "$slot" ]; then - ${pkgs.coreutils}/bin/install -m 0600 -o "$uid" -g root /dev/null "$slot" - else - ${pkgs.coreutils}/bin/chown "$uid":root "$slot" - ${pkgs.coreutils}/bin/chmod 0600 "$slot" - fi - done - done - ''; - system.activationScripts.d2bGroupMigration = lib.stringAfter [ "users" ] '' target_gid="$(${pkgs.getent}/bin/getent group d2b | ${pkgs.coreutils}/bin/cut -d: -f3)" @@ -487,17 +419,13 @@ in # entries produces the same ACL state. Runs after every # `nixos-rebuild switch` so a bundle update with new role # UIDs is automatically reflected. - system.activationScripts.d2bRoleUidAcls = lib.stringAfter [ "users" "d2bGuestControlTokens" ] '' + system.activationScripts.d2bRoleUidAcls = lib.stringAfter [ "users" ] '' set +u bundle_json=/etc/d2b/processes.json if [ -r "$bundle_json" ]; then ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _: '' - guest_control_virtiofsd_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.id == "virtiofsd-d2b-gctl") | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) - guest_control_ch_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.id == "cloud-hypervisor") | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) qemu_media_session_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.role == "qemu-media-runner") | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) - ${activationHelper} clear-acl-on-path --path "/var/lib/d2b/guest-control-${name}" --require-kind directory --setfacl-bin "${pkgs.acl}/bin/setfacl" 2>/dev/null || true - ${activationHelper} clear-acl-on-path --path "/var/lib/d2b/guest-control-${name}/token" --require-kind regular --setfacl-bin "${pkgs.acl}/bin/setfacl" 2>/dev/null || true qemu_media_acl_mask_repair=0 for uid in $qemu_media_session_uids; do [ "$uid" = "0" ] && continue @@ -509,55 +437,6 @@ in ${pkgs.acl}/bin/setfacl -m "u:$uid:rwx" /run/d2b/vms/${name} 2>/dev/null || true ${pkgs.acl}/bin/setfacl -d -m "u:$uid:rwx" /run/d2b/vms/${name} 2>/dev/null || true done - for uid in $guest_control_virtiofsd_uids; do - [ "$uid" = "0" ] && continue - ${pkgs.acl}/bin/setfacl -m "u:$uid:x" /var/lib/d2b 2>/dev/null || true - ${pkgs.acl}/bin/setfacl -m "u:$uid:x" /run/d2b 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/var/lib/d2b/guest-control-${name}" \ - --acl-spec "u:$uid:rx" \ - --also-spec "mask:r-x" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/var/lib/d2b/guest-control-${name}/token" \ - --acl-spec "u:$uid:r" \ - --also-spec "mask:r--" \ - --require-kind regular \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - d2bd_uid=$(${pkgs.getent}/bin/getent passwd d2bd | ${pkgs.coreutils}/bin/cut -d: -f3) - d2b_gid=$(${pkgs.getent}/bin/getent group d2b | ${pkgs.coreutils}/bin/cut -d: -f3) - if [ -n "$d2bd_uid" ] && [ -n "$d2b_gid" ]; then - ${activationHelper} enforce-dir-posture --path /run/d2b/vms/${name} --uid "$d2bd_uid" --gid "$d2b_gid" --mode 0750 2>/dev/null || true - ${activationHelper} enforce-dir-posture --path /run/d2b/vms/${name}/guest-control --uid "$d2bd_uid" --gid "$d2b_gid" --mode 0750 2>/dev/null || true - fi - ${activationHelper} clear-acl-on-path --path /run/d2b/vms/${name}/guest-control --require-kind directory --setfacl-bin "${pkgs.acl}/bin/setfacl" 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/run/d2b/vms/${name}" \ - --acl-spec "u:$uid:--x" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/run/d2b/vms/${name}/guest-control" \ - --acl-spec "u:$uid:rwx" \ - --also-spec "default:u:$uid:rwx" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - for ch_uid in $guest_control_ch_uids; do - [ "$ch_uid" = "0" ] && continue - ${activationHelper} setfacl-on-path \ - --path "/run/d2b/vms/${name}/guest-control" \ - --acl-spec "u:$ch_uid:--x" \ - --also-spec "default:u:$ch_uid:rwX" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - done - done if [ -d /var/lib/d2b/vms/${name} ]; then # Security fix: # narrow /dev/kvm ACL to only KVM-consuming role UIDs, and @@ -578,7 +457,10 @@ in # audio → PipeWire/Pulse only (no Wayland) # gpu/gpu-render-node → Wayland only when no proxy is emitted, # no session socket grant when proxy is active - wlproxy_wayland_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.role == "wayland-proxy") | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) + # Only the Host execution owns the real compositor socket. The + # Guest frontend uses the same logical role for bundle/profile + # lookup but must never receive host Wayland ACLs. + wlproxy_wayland_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.role == "wayland-proxy" and ((.executionRef // "") | startswith("Guest/") | not)) | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) audio_session_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.role == "audio") | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) gpu_session_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.role == "gpu" or .role == "gpu-render-node") | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) wlproxy_client_uids=$(printf '%s\n%s\n' "$gpu_session_uids" "$qemu_media_session_uids" | ${pkgs.coreutils}/bin/sort -u) @@ -587,7 +469,6 @@ in overlay_uid=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | .planOps[]? | select(.kind == "diskInit" and (.targetPath | endswith("/store-overlay.img"))) | .ownerUid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/head -n1) overlay_gid=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | .planOps[]? | select(.kind == "diskInit" and (.targetPath | endswith("/store-overlay.img"))) | .ownerGid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/head -n1) overlay_size_mib=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | .planOps[]? | select(.kind == "diskInit" and (.targetPath | endswith("/store-overlay.img"))) | (.sizeBytes / 1048576 | floor)' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/head -n1) - guest_control_virtiofsd_uids=$(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(.id == "virtiofsd-d2b-gctl") | .profile.uid' "$bundle_json" 2>/dev/null | ${pkgs.coreutils}/bin/sort -u) if [ -n "$overlay_uid" ] && [ "$overlay_uid" != "null" ] && \ [ -n "$overlay_gid" ] && [ "$overlay_gid" != "null" ] && \ [ -n "$overlay_size_mib" ] && [ "$overlay_size_mib" != "null" ]; then @@ -616,62 +497,13 @@ in done fi fi - for uid in $(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | .profile.uid' "$bundle_json" | ${pkgs.coreutils}/bin/sort -u); do + for uid in $(${pkgs.jq}/bin/jq -r '.vms[] | select(.vm == "${name}") | .nodes[] | select(((.executionRef // "") | startswith("Guest/") | not)) | .profile.uid' "$bundle_json" | ${pkgs.coreutils}/bin/sort -u); do [ "$uid" = "0" ] && continue ${pkgs.acl}/bin/setfacl -m "u:$uid:x" /var/lib/d2b 2>/dev/null || true # Grant traversal on both shared runtime parents so numeric # per-role UIDs can reach /run/d2b/vms/ sockets. ${pkgs.acl}/bin/setfacl -m "u:$uid:x" /run/d2b 2>/dev/null || true ${pkgs.acl}/bin/setfacl -m "u:$uid:x" /run/d2b/vms 2>/dev/null || true - if echo "$guest_control_virtiofsd_uids" | ${pkgs.gnugrep}/bin/grep -qx "$uid"; then - ${activationHelper} clear-acl-on-path --path "/var/lib/d2b/guest-control-${name}" --require-kind directory --setfacl-bin "${pkgs.acl}/bin/setfacl" 2>/dev/null || true - ${activationHelper} clear-acl-on-path --path "/var/lib/d2b/guest-control-${name}/token" --require-kind regular --setfacl-bin "${pkgs.acl}/bin/setfacl" 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/var/lib/d2b/guest-control-${name}" \ - --acl-spec "u:$uid:rx" \ - --also-spec "mask:r-x" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/var/lib/d2b/guest-control-${name}/token" \ - --acl-spec "u:$uid:r" \ - --also-spec "mask:r--" \ - --require-kind regular \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - d2bd_uid=$(${pkgs.getent}/bin/getent passwd d2bd | ${pkgs.coreutils}/bin/cut -d: -f3) - d2b_gid=$(${pkgs.getent}/bin/getent group d2b | ${pkgs.coreutils}/bin/cut -d: -f3) - if [ -n "$d2bd_uid" ] && [ -n "$d2b_gid" ]; then - ${activationHelper} enforce-dir-posture --path /run/d2b/vms/${name} --uid "$d2bd_uid" --gid "$d2b_gid" --mode 0750 2>/dev/null || true - ${activationHelper} enforce-dir-posture --path /run/d2b/vms/${name}/guest-control --uid "$d2bd_uid" --gid "$d2b_gid" --mode 0750 2>/dev/null || true - fi - ${activationHelper} clear-acl-on-path --path /run/d2b/vms/${name}/guest-control --require-kind directory --setfacl-bin "${pkgs.acl}/bin/setfacl" 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/run/d2b/vms/${name}" \ - --acl-spec "u:$uid:--x" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - ${activationHelper} setfacl-on-path \ - --path "/run/d2b/vms/${name}/guest-control" \ - --acl-spec "u:$uid:rwx" \ - --also-spec "default:u:$uid:rwx" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - for ch_uid in $guest_control_ch_uids; do - [ "$ch_uid" = "0" ] && continue - ${activationHelper} setfacl-on-path \ - --path "/run/d2b/vms/${name}/guest-control" \ - --acl-spec "u:$ch_uid:--x" \ - --also-spec "default:u:$ch_uid:rwX" \ - --require-kind directory \ - --setfacl-bin "${pkgs.acl}/bin/setfacl" \ - 2>/dev/null || true - done - continue - fi if echo "$otel_host_bridge_uids" | ${pkgs.gnugrep}/bin/grep -qx "$uid"; then ${pkgs.acl}/bin/setfacl -m "u:$uid:rwx" /run/d2b/otel 2>/dev/null || true ${pkgs.acl}/bin/setfacl -d -m "u:$uid:rwx" /run/d2b/otel 2>/dev/null || true diff --git a/nixos-modules/host-broker.nix b/nixos-modules/host-broker.nix index 9e53d992e6..90e2161445 100644 --- a/nixos-modules/host-broker.nix +++ b/nixos-modules/host-broker.nix @@ -1,4 +1,4 @@ -# d2b-priv-broker.{socket,service} +# d2b-broker.{socket,service} # # Socket-activated privileged broker per ADR 0001 (`unsafe_code = # "deny"` quarantine boundary). Daemon connects to /run/d2b/priv.sock @@ -31,21 +31,10 @@ let overrides = d2bHostToolOverrides; key = "broker"; fallback = - if prebuilt ? "d2b-priv-broker" - then prebuilt."d2b-priv-broker" + if prebuilt ? "selectPackage" + then prebuilt.selectPackage "d2b-broker" brokerSourcePackage else brokerSourcePackage; }; - cutoverRunnerSourcePackage = d2bHostTools.cutoverRunner; - cutoverRunnerPackage = d2bLib.selectHostToolPackage { - overrides = d2bHostToolOverrides; - key = "cutoverRunner"; - fallback = - if prebuilt ? "d2b-cutover-runner" - then prebuilt."d2b-cutover-runner" - else cutoverRunnerSourcePackage; - }; - cutoverRunnerPath = "${cutoverRunnerPackage}/bin/d2b-cutover-runner"; - bundleManifestPath = cfg.site.bundle.currentManifest or "/etc/d2b/bundle.json"; @@ -93,7 +82,6 @@ let D2B_BROKER_NFT_BINARY = "${pkgs.nftables}/bin/nft"; D2B_BROKER_IP_BINARY = "${pkgs.iproute2}/bin/ip"; D2B_BROKER_USBIP_BINARY = "${pkgs.linuxPackages_latest.usbip}/bin/usbip"; - D2B_CUTOVER_RUNNER_PATH = cutoverRunnerPath; }; path = with pkgs; [ nftables @@ -144,7 +132,8 @@ let SystemCallArchitectures = "native"; UMask = "0027"; ExecStart = - "${brokerPackage}/bin/d2b-priv-broker serve " + + "${brokerPackage}/bin/d2b-broker host " + + "--authority-id realm-${realm.id} " + "--audit-dir ${realm.controller.broker.auditDir} " + "--audit-retention-days ${toString auditRetentionDays} " + "--bundle-path ${bundleManifestPath} " + @@ -157,7 +146,7 @@ let RestartSec = "2s"; StandardOutput = "journal"; StandardError = "journal"; - SyslogIdentifier = "d2b-realm-priv-broker"; + SyslogIdentifier = "d2b-realm-broker"; }; }; realmBrokerSockets = lib.listToAttrs (map @@ -192,9 +181,8 @@ in # `docs/how-to/migrate-nixos-to-daemon.md` § Recovery. config = { - environment.systemPackages = [ brokerPackage cutoverRunnerPackage ]; - d2b._hostToolPackages.d2bCutoverRunner = cutoverRunnerPackage; - d2b._bundle.cutoverRunnerPath = cutoverRunnerPath; + environment.systemPackages = [ brokerPackage ]; + d2b._hostToolPackages.d2bBroker = brokerPackage; # broker-owned state + bundle dirs; /run/d2b itself is owned by # root:d2b 1770 with explicit ACLs from host-daemon.nix (canonical; @@ -227,7 +215,7 @@ in # via SD_LISTEN_FDS=1 LISTEN_FDS=1 LISTEN_FDNAMES=priv.sock and # MUST NOT bind/listen itself when activated this way. systemd.sockets = { - d2b-priv-broker = { + d2b-broker = { description = "d2b privileged broker socket"; wantedBy = [ "sockets.target" ]; requires = [ "systemd-tmpfiles-setup.service" ]; @@ -244,7 +232,7 @@ in } // realmBrokerSockets; systemd.services = { - d2b-priv-broker = { + d2b-broker = { description = "d2b privileged broker (uid 0 host-mutation surface)"; documentation = [ "https://github.com/vicondoa/d2b/blob/main/docs/adr/0002-non-root-daemon-and-privileged-broker.md" @@ -253,11 +241,11 @@ in # Socket-activated; service activation comes from the socket unit. # No wantedBy here. requires = [ - "d2b-priv-broker.socket" + "d2b-broker.socket" "systemd-tmpfiles-setup.service" ]; after = [ - "d2b-priv-broker.socket" + "d2b-broker.socket" "systemd-tmpfiles-setup.service" "local-fs.target" ]; @@ -273,7 +261,6 @@ in D2B_BROKER_IP_BINARY = "${pkgs.iproute2}/bin/ip"; # usbip binary from linuxPackages_latest.usbip. D2B_BROKER_USBIP_BINARY = "${pkgs.linuxPackages_latest.usbip}/bin/usbip"; - D2B_CUTOVER_RUNNER_PATH = cutoverRunnerPath; }; # ApplyNftables / SpawnRunner mount-prep ops invoke nft / @@ -290,7 +277,7 @@ in # Type=notify so the daemon can deterministically observe # READY=1 after the broker has adopted the listen fd and # completed cgroup delegation. Pair with sd_notify(READY=1) - # in packages/d2b-priv-broker/src/runtime.rs after the + # in packages/d2b-broker/src/runtime.rs after the # SD_LISTEN_FDS adoption + cgroup delegation sequence. # Type = "notify"; @@ -389,11 +376,11 @@ in # must be optional for ExecStartPre: systemd loads EnvironmentFile # before every command in the service, and ExecStartPre is what # creates the file before ExecStart consumes it. - EnvironmentFile = "-/run/d2b/broker/priv-broker.env"; - ExecStartPre = "+${pkgs.writeShellScript "d2b-priv-broker-prep" '' + EnvironmentFile = "-/run/d2b/broker/broker.env"; + ExecStartPre = "+${pkgs.writeShellScript "d2b-broker-prep" '' set -euo pipefail - env_file=/run/d2b/broker/priv-broker.env - env_tmp=/run/d2b/broker/priv-broker.env.new + env_file=/run/d2b/broker/broker.env + env_tmp=/run/d2b/broker/broker.env.new uid=$(${pkgs.coreutils}/bin/id -u d2bd) gid=$(${pkgs.coreutils}/bin/id -g d2bd) umask 0077 @@ -410,7 +397,8 @@ in # MUST adopt the inherited fd; the --socket-path flag is the # non-activated-mode fallback only. ExecStart = - "${brokerPackage}/bin/d2b-priv-broker serve " + + "${brokerPackage}/bin/d2b-broker host " + + "--authority-id host " + "--audit-dir /var/lib/d2b/audit " + "--audit-retention-days ${toString auditRetentionDays} " + "--bundle-path ${bundleManifestPath} " + @@ -423,7 +411,7 @@ in StandardOutput = "journal"; StandardError = "journal"; - SyslogIdentifier = "d2b-priv-broker"; + SyslogIdentifier = "d2b-broker"; }; }; @@ -443,8 +431,8 @@ in # `daemonExperimental.enable = false` drops the daemon config), # since systemd merges these at the unit-file level. d2bd = { - wants = [ "d2b-priv-broker.socket" ]; - after = [ "d2b-priv-broker.socket" ]; + wants = [ "d2b-broker.socket" ]; + after = [ "d2b-broker.socket" ]; }; } // realmBrokerServices; }; diff --git a/nixos-modules/host-daemon.nix b/nixos-modules/host-daemon.nix index e58c243dd8..d55673f70c 100644 --- a/nixos-modules/host-daemon.nix +++ b/nixos-modules/host-daemon.nix @@ -236,14 +236,14 @@ let wants = [ "systemd-tmpfiles-setup.service" - "d2b-priv-broker.socket" + "d2b-broker.socket" ] ++ lib.optional (brokerSocketUnit != null) brokerSocketUnit; after = [ "systemd-tmpfiles-setup.service" - "d2b-priv-broker.socket" - "d2b-priv-broker.service" + "d2b-broker.socket" + "d2b-broker.service" "network.target" "dbus.socket" "dbus.service" @@ -260,7 +260,7 @@ let User = realm.controller.daemon.user; Group = realm.controller.daemon.group; ExecStart = - "${d2bdPackage}/bin/d2bd serve " + + "${d2bdPackage}/bin/d2bd host " + "--config ${realm.controller.daemon.configPath} " + "--daemon-state-dir ${realm.paths.stateDir}"; TimeoutStopSec = lib.mkDefault "${toString d2bdStopTimeoutSeconds}s"; @@ -479,14 +479,14 @@ in description = "d2b daemon skeleton"; wantedBy = [ "multi-user.target" ]; wants = [ - "d2b-priv-broker.socket" + "d2b-broker.socket" "systemd-tmpfiles-setup.service" ]; after = [ "systemd-tmpfiles-setup.service" "network.target" - "d2b-priv-broker.socket" - "d2b-priv-broker.service" + "d2b-broker.socket" + "d2b-broker.service" "dbus.socket" "dbus.service" "d2b.slice" @@ -512,7 +512,7 @@ in # subtree to the daemon user"). User = "d2bd"; Group = "d2bd"; - ExecStart = "${d2bdPackage}/bin/d2bd serve --config /etc/d2b/daemon-config.json"; + ExecStart = "${d2bdPackage}/bin/d2bd host --config /etc/d2b/daemon-config.json"; ExecStop = "+${hostShutdownHook}"; TimeoutStopSec = lib.mkDefault "${toString d2bdStopTimeoutSeconds}s"; Restart = "on-failure"; @@ -522,10 +522,9 @@ in AmbientCapabilities = [ "" ]; PrivateTmp = true; ProtectHome = true; - # AF_UNIX: public.sock + broker IPC + the per-VM guest-control - # vsock proxy socket the daemon-side authenticated Health probe - # connects to. AF_INET/AF_INET6 remain for the daemon's other TCP - # readiness probes (e.g. per-env usbipd backend ports). + # AF_UNIX: public.sock and broker IPC. AF_INET/AF_INET6 remain for + # the daemon's other TCP readiness probes (e.g. per-env usbipd + # backend ports); Guest ComponentSession transport is target-local. RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; UMask = "0027"; # Supplementary group so the daemon can create diff --git a/nixos-modules/host-polkit.nix b/nixos-modules/host-polkit.nix index b8ec04a683..1308fd8866 100644 --- a/nixos-modules/host-polkit.nix +++ b/nixos-modules/host-polkit.nix @@ -33,7 +33,7 @@ let # * `d2bd.service` - the public daemon. It may restart after a # `nixos-rebuild switch`; VM runners survive via KillMode=process # and daemon re-adoption. - # * `d2b-priv-broker.service` + `d2b-priv-broker.socket` - + # * `d2b-broker.service` + `d2b-broker.socket` - # the privileged broker pair. Socket-activated; operators may # bounce them to recover from a stuck handler. # @@ -51,8 +51,8 @@ let # --------------------------------------------------------------------------- launcherAllowedUnits = [ "d2bd.service" - "d2b-priv-broker.service" - "d2b-priv-broker.socket" + "d2b-broker.service" + "d2b-broker.socket" ]; launcherAllowedUnitsJs = diff --git a/nixos-modules/host.nix b/nixos-modules/host.nix index d1989c081b..a011481a99 100644 --- a/nixos-modules/host.nix +++ b/nixos-modules/host.nix @@ -1,6 +1,6 @@ { inputs }: -{ config, pkgs, lib, ... }: +{ config, pkgs, lib, d2bHostTools ? null, d2bHostToolOverrides ? null, ... }: let d2bLib = import ./lib.nix { inherit lib; }; @@ -14,7 +14,7 @@ let # lib.nix helpers (vmRunner / vmToplevel / vmDeclaredRunner) # read it. vmSubmodule = (import ./vm-submodule.nix { inherit inputs; }) - { inherit config lib pkgs; }; + { inherit config lib pkgs d2bHostTools d2bHostToolOverrides; }; composeVm = vmSubmodule._composeVm; cfg = config.d2b; @@ -194,13 +194,6 @@ in tag = "d2b-obs-sec"; proto = "virtiofs"; }; - guestControlShare = lib.optional vm'.guest.control.enable { - source = "${cfg.site.stateDir}/guest-control-${name}"; - mountPoint = "/run/d2b-guest-control-host"; - tag = "d2b-gctl"; - proto = "virtiofs"; - readOnly = true; - }; composedModules = [ # Framework guest baseline + d2b-managed sshd host keys. ./base.nix @@ -231,14 +224,14 @@ in # the GuestConfig target and exists independently of any # SSH metadata. `ssh.user` only chooses ownership when it # is set; when absent the framework defaults ownership to - # root (guestd reads the copy, and the guest-control exec - # path edits it, as root). + # root (the Guest config-nixos service reads the copy, and the + # target-local Process path edits it under its own policy). owner = if vm'.ssh.user != null then vm'.ssh.user else "root"; in { environment.etc."d2b/guest-config.nix".source = vm'.guestConfigFile; systemd.tmpfiles.rules = [ - "d /var/lib/d2b-guest 0750 ${owner} users -" - "C /var/lib/d2b-guest/guest-config.nix 0640 ${owner} users - /etc/d2b/guest-config.nix" + "d /var/lib/d2b-guest 0750 ${owner} d2bd -" + "C /var/lib/d2b-guest/guest-config.nix 0640 ${owner} d2bd - /etc/d2b/guest-config.nix" ]; } ) @@ -276,25 +269,16 @@ in { d2b.sshUser = vm'.ssh.user; d2b.sudo = vm'.sudo; - d2b.guestControl.enable = vm'.guest.control.enable; + d2b.componentSession.enable = vm'.guest.componentSession.enable; # D17: thread the operator-editable working-copy path into - # the guest independently of ssh.user so guestd advertises - # the ReadGuestFile capability exactly when there is a + # the guest independently of ssh.user so config-nixos exposes + # the bounded GuestConfig capability exactly when there is a # guestConfigFile to sync. - d2b.guestControl.guestConfigPath = + d2b.componentSession.guestConfigPath = if vm'.guestConfigFile != null then "/var/lib/d2b-guest/guest-config.nix" else null; - d2b.guestControl.exec = { - enable = lib.mkForce vm'.guest.exec.enable; - # The host-fixed workload user every exec runs as (never root), - # derived from the per-VM workload user. guestd runs every exec - # as this user in a PAM login session. - execUser = lib.mkForce vm'.ssh.user; - detachedMaxRuntimeSec = lib.mkForce vm'.guest.exec.detachedMaxRuntimeSec; - interactiveMaxRuntimeSec = lib.mkForce vm'.guest.exec.interactiveMaxRuntimeSec; - }; - d2b.guestControl.shell = { + d2b.componentSession.shell = { enable = lib.mkForce vm'.guest.shell.enable; defaultName = lib.mkForce vm'.guest.shell.defaultName; maxSessions = lib.mkForce vm'.guest.shell.maxSessions; @@ -344,7 +328,7 @@ in tag = "d2b-ssh-host"; proto = "virtiofs"; } - ] ++ obsSecretsShare ++ guestControlShare); + ] ++ obsSecretsShare); } ]; in (composeVm name composedModules) // { diff --git a/nixos-modules/index.nix b/nixos-modules/index.nix index 3ecdf79825..6c347e5e21 100644 --- a/nixos-modules/index.nix +++ b/nixos-modules/index.nix @@ -400,8 +400,8 @@ let user = "root"; group = principal; socketPath = realm.paths.brokerSocket; - socketUnitName = "${unitPrefix}-priv-broker.socket"; - serviceUnitName = "${unitPrefix}-priv-broker.service"; + socketUnitName = "${unitPrefix}-broker.socket"; + serviceUnitName = "${unitPrefix}-broker.service"; auditDir = realm.paths.auditDir; materializedSocket = brokerMaterialized; materializedService = brokerMaterialized; @@ -520,7 +520,7 @@ let # v3 cross-Zone index. This is deliberately derived from the unified # resource declarations rather than from the legacy env/VM tables above. - # The legacy index remains available to the pre-cutover emitters until + # The legacy index remains available to the compatibility emitters until # their consumers move to the v3 artifact. v3ResourceRows = zoneName: zone: lib.mapAttrsToList @@ -846,8 +846,6 @@ let defaultName = vm.guest.shell.defaultName; maxSessions = vm.guest.shell.maxSessions; maxAttached = vm.guest.shell.maxAttached; - controlEnabled = vm.guest.control.enable; - execEnabled = vm.guest.exec.enable; }) shellVms; }; diff --git a/nixos-modules/lib.nix b/nixos-modules/lib.nix index 1350ff8605..1f2eb4283e 100644 --- a/nixos-modules/lib.nix +++ b/nixos-modules/lib.nix @@ -71,7 +71,6 @@ let "broker" "activationHelper" "hostActivationHelper" - "cutoverRunner" "unsafeLocalHelper" "resourceCompiler" "waylandProxy" @@ -263,7 +262,6 @@ rec { lifecycle = true; display = true; usbHotplug = true; - guestControl = true; exec = true; configSync = true; ssh = true; @@ -293,7 +291,6 @@ rec { waylandProxy = true; }; guest = { - guestControl = true; exec = true; shell = true; configSync = true; @@ -313,7 +310,6 @@ rec { lifecycle = true; display = true; usbHotplug = true; - guestControl = false; exec = false; configSync = false; ssh = false; @@ -343,7 +339,6 @@ rec { waylandProxy = false; }; guest = { - guestControl = false; exec = false; shell = false; configSync = false; @@ -393,7 +388,7 @@ rec { (runtimeServiceSummary { id = "store-virtiofs-preflight"; role = "storage"; }) (runtimeServiceSummary { id = "virtiofsd"; role = "storage"; }) (runtimeServiceSummary { id = "cloud-hypervisor"; role = "hypervisor"; }) - (runtimeServiceSummary { id = "guest-control-health"; role = "guest-control"; }) + (runtimeServiceSummary { id = "component-session"; role = "component-session"; }) (runtimeServiceSummary { id = "swtpm"; role = "tpm"; optional = true; }) (runtimeServiceSummary { id = "gpu"; role = "display"; optional = true; }) (runtimeServiceSummary { id = "audio"; role = "audio"; optional = true; }) @@ -476,7 +471,7 @@ rec { neededForBoot = true; }; - guestControlVsockPort = 14318; + componentSessionVsockPort = 14318; observabilityOtlpVsockPort = 14317; # AF_VSOCK port used by the d2b security-key CTAPHID relay frontend. # The guest sk-frontend connects on this port to the host broker. @@ -487,7 +482,7 @@ rec { # reserve slot 1 for the env net VM and use d2b.vms..index # for workloads (10..250). The stride intentionally exceeds the # maximum workload index so adjacent envs cannot collide. - guestControlVsockCid = { name, envIndex ? null, index ? null, isNetVm ? false, isObservabilityVm ? false }: + componentSessionVsockCid = { name, envIndex ? null, index ? null, isNetVm ? false, isObservabilityVm ? false }: if isObservabilityVm then observabilityStackVsockCid else if envIndex != null then let slot = if isNetVm then 1 else index; in @@ -495,7 +490,7 @@ rec { else 4096 + lib.fromHexString (builtins.substring 0 6 (builtins.hashString "md5" name)); - guestControlVsockHostSocket = stateRoot: "${stateRoot}/vsock.sock"; + componentSessionVsockHostSocket = stateRoot: "${stateRoot}/vsock.sock"; volumeSerialIssues = volumes: let diff --git a/nixos-modules/manifest.nix b/nixos-modules/manifest.nix index d30dbdf71f..1340a0855c 100644 --- a/nixos-modules/manifest.nix +++ b/nixos-modules/manifest.nix @@ -104,14 +104,14 @@ let then envIndexMap.${envName} else null; baseVsockCid = - if isNixosRuntime then d2bLib.guestControlVsockCid { + if isNixosRuntime then d2bLib.componentSessionVsockCid { inherit name envIndex; index = vm.index; isNetVm = asNetVmForEnv != null; isObservabilityVm = obsCfg.enable && name == obsCfg.vmName; } else null; baseVsockHostSocket = - if isNixosRuntime then d2bLib.guestControlVsockHostSocket stateRoot else null; + if isNixosRuntime then d2bLib.componentSessionVsockHostSocket stateRoot else null; in { inherit name; @@ -227,7 +227,7 @@ let vmName = obsCfg.vmName; obsVsockCid = 1000; obsVsockHostSocket = - d2bLib.guestControlVsockHostSocket "${config.d2b.store.stateDir}/${obsCfg.vmName}"; + d2bLib.componentSessionVsockHostSocket "${config.d2b.store.stateDir}/${obsCfg.vmName}"; signozUrl = "http://${obsCfg.signoz.listenAddress}:${toString obsCfg.signoz.listenPort}"; signozOtlpGrpcPort = obsCfg.signoz.otlpGrpcPort; signozOtlpHttpPort = obsCfg.signoz.otlpHttpPort; @@ -266,7 +266,6 @@ let lifecycle = lib.mkOption { type = lib.types.bool; }; display = lib.mkOption { type = lib.types.bool; }; usbHotplug = lib.mkOption { type = lib.types.bool; }; - guestControl = lib.mkOption { type = lib.types.bool; }; exec = lib.mkOption { type = lib.types.bool; }; configSync = lib.mkOption { type = lib.types.bool; }; ssh = lib.mkOption { type = lib.types.bool; }; @@ -316,7 +315,6 @@ let type = lib.types.submodule { freeformType = null; options = { - guestControl = lib.mkOption { type = lib.types.bool; }; exec = lib.mkOption { type = lib.types.bool; }; shell = lib.mkOption { type = lib.types.bool; }; configSync = lib.mkOption { type = lib.types.bool; }; @@ -353,7 +351,7 @@ let "audio" "video" "network" - "guest-control" + "component-session" "usb" "observability" ]; @@ -469,7 +467,7 @@ let Hypervisor VMs. Env-backed VMs use `100 + envIndex * 1000 + slot`, where slot 1 is reserved for the env net VM and workload VMs use `d2b.vms..index`. Null for providers that do not expose - d2b guest-control or in-guest observability. + d2b component-session or in-guest observability. ''; }; @@ -477,7 +475,7 @@ let type = lib.types.nullOr lib.types.str; description = '' Host-side Unix socket backing this VM's Cloud Hypervisor vsock - device. Null for providers without d2b guest-control or + device. Null for providers without d2b component-session or in-guest observability. ''; }; @@ -733,8 +731,8 @@ let type = lib.types.nullOr manifestShellType; description = '' Persistent guest shell policy metadata for providers that support the - authenticated guest-control terminal substrate. Null for runtime - providers without d2b guest-control. + authenticated component-session terminal substrate. Null for runtime + providers without d2b component-session. ''; }; }; @@ -804,7 +802,7 @@ in * 4 - base Cloud Hypervisor vsock semantics. Keeps the v3 shape, but defines per-VM `observability.vsockCid` and `observability.vsockHostSocket` as the host-owned base - vsock device used by observability today and guest control in + vsock device used by observability today and ComponentSession in later waves. Pinned by `d2b_core::manifest_v04::MANIFEST_VERSION_CURRENT`; the broker / daemon refuse any other value with a @@ -823,7 +821,7 @@ in * 6 - adds per-VM runtime/provider metadata and provider capability summaries, and makes provider-specific socket/vsock fields nullable so qemu-media entries do not fabricate Cloud - Hypervisor, guest-control, SSH, store-sync, key, or + Hypervisor, component-session, SSH, store-sync, key, or in-guest-observability artifacts. * 7 - adds per-VM lifecycle.gracefulShutdown metadata so d2bd can apply VM-specific graceful guest-shutdown policy while diff --git a/nixos-modules/minijail-profiles.nix b/nixos-modules/minijail-profiles.nix index f685fbb94e..d8e9be916b 100644 --- a/nixos-modules/minijail-profiles.nix +++ b/nixos-modules/minijail-profiles.nix @@ -189,10 +189,7 @@ let let shareTag = builtins.unsafeDiscardStringContext share.tag; shareNodeId = "virtiofsd-${shareTag}"; - principal = - if shareTag == "d2b-gctl" - then "d2b-${name}-gctlfs" - else "d2b-${name}-runner"; + principal = "d2b-${name}-runner"; in { name = profileIdFor name shareNodeId; value = mkProfile { @@ -207,15 +204,11 @@ let # no CAP_SETUID, no CAP_SYS_ADMIN on the host. capabilities = [ ]; seccompPolicyRef = "w1-virtiofsd"; - readOnlyPaths = [ "/nix/store" ] - ++ lib.optional (shareTag == "d2b-gctl") share.source; - writablePaths = - if shareTag == "d2b-gctl" then [ - (mkWritablePath "${audioRuntimeDirOf name}/guest-control" "Expose the guest-control token virtiofs socket.") - ] else [ - (mkWritablePath (stateDirOf name) "Materialize virtiofs sockets and VM-local store state.") - (mkWritablePath (runtimeDirOf name) "Expose broker-prepared virtiofs runtime sockets.") - ]; + readOnlyPaths = [ "/nix/store" ]; + writablePaths = [ + (mkWritablePath (stateDirOf name) "Materialize virtiofs sockets and VM-local store state.") + (mkWritablePath (runtimeDirOf name) "Expose broker-prepared virtiofs runtime sockets.") + ]; cgroupSubtree = "d2b.slice/${name}/${shareNodeId}"; controllers = serviceControllers; # (ADR 0021): broker pre-creates a user NS @@ -340,12 +333,28 @@ let controllers = serviceControllers; }; - "${profileIdFor name "guest-control-health"}" = mkProfile { - profileId = profileIdFor name "guest-control-health"; - role = "guest-control-health"; - principal = "d2bd"; - seccompPolicyRef = "w1-guest-control-health"; - cgroupSubtree = "d2b.slice/${name}/guest-control-health"; + "${profileIdFor name "activation-nixos-runner"}" = mkProfile { + profileId = profileIdFor name "activation-nixos-runner"; + role = "activation-nixos-runner"; + principal = "d2b-${name}-activation"; + uid = 0; + gid = 0; + seccompPolicyRef = "w1-activation-nixos-runner"; + namespaces = defaultNamespaces // { + pid = true; + }; + adr_carve_out = "Target-local activation helper requires its declared root identity inside the Guest sandbox."; + readOnlyPaths = [ + "/etc/d2b" + "/nix/store" + "/nix/var/nix/profiles" + "/run/current-system" + ]; + writablePaths = [ + (mkWritablePath (stateDirOf name) "Write only target-local activation state.") + ]; + cgroupSubtree = "d2b.slice/${name}/activation-nixos-runner"; + controllers = serviceControllers; }; } // lib.optionalAttrs vm.tpm.enable { @@ -515,7 +524,7 @@ let # deviceBinds is intentionally empty: /dev/dri/renderD128 is # pre-opened by the broker parent and passed to the user-NS child # via fd inheritance (RENDER_NODE_INHERITED_FD = 10 protocol - # constant in d2b-priv-broker/src/sys.rs). No bind-mount. + # constant in d2b-broker/src/sys.rs). No bind-mount. deviceBinds = [ ]; # No real host compositor bind-mount: the GPU runner connects to # the per-VM proxy socket at /run/d2b-wlproxy//wayland-0. @@ -583,6 +592,29 @@ let umask = 7; }; } + // lib.optionalAttrs (vm.graphics.enable && vm.graphics.crossDomainTrusted) { + # Guest frontend workers are resolved by the Guest systemd Provider. + # Keep the signed profile deliberately systemd-compatible: no host + # namespaces, capabilities, devices, or writable host paths. + "${profileIdFor name "wayland-frontend-worker"}" = mkProfile { + profileId = profileIdFor name "wayland-frontend-worker"; + role = "wayland-proxy"; + principal = "d2b-${name}-wlfrontend"; + capabilities = [ ]; + namespaces = { + ipc = false; + mount = false; + net = false; + pid = false; + user = false; + uts = false; + }; + seccompPolicyRef = "strict"; + cgroupSubtree = "d2b.slice/${name}/wayland-frontend-worker"; + controllers = serviceControllers; + umask = 18; + }; + } // lib.optionalAttrs vm.audio.enable { "${profileIdFor name "audio"}" = mkProfile { profileId = profileIdFor name "audio"; @@ -696,7 +728,6 @@ let profileId = profileIdFor name "sk-frontend"; role = "security-key-frontend"; principal = "d2bd"; - seccompPolicyRef = "w1-guest-control-health"; cgroupSubtree = "d2b.slice/${name}/sk-frontend"; controllers = serviceControllers; }; @@ -849,11 +880,13 @@ let # container UID 0 maps to another role's host identity). # Walk every principal in fullProfileTable, group by UID, and # fail eval if any UID has more than one distinct principal. - principalUidPairs = lib.flatten (lib.mapAttrsToList - (profileId: data: [ - { principal = data.principal; uid = data.uid; profileId = profileId; } - ]) - fullProfileTable); + principalUidPairs = lib.filter + (pair: pair.uid == stablePrincipalId pair.principal) + (lib.flatten (lib.mapAttrsToList + (profileId: data: [ + { principal = data.principal; uid = data.uid; profileId = profileId; } + ]) + fullProfileTable)); principalUidByUid = lib.foldl' (acc: pair: let diff --git a/nixos-modules/options-gateway.nix b/nixos-modules/options-gateway.nix index 63cfec4a65..18bc5c8d59 100644 --- a/nixos-modules/options-gateway.nix +++ b/nixos-modules/options-gateway.nix @@ -19,18 +19,18 @@ in description = "Internal: resolved host d2b CLI package."; }; - d2bCutoverRunner = lib.mkOption { + d2bd = lib.mkOption { type = lib.types.nullOr lib.types.package; default = null; internal = true; - description = "Internal: resolved one-shot host cutover runner package."; + description = "Internal: resolved host d2bd package."; }; - d2bd = lib.mkOption { + d2bBroker = lib.mkOption { type = lib.types.nullOr lib.types.package; default = null; internal = true; - description = "Internal: resolved host d2bd package."; + description = "Internal: resolved shared d2b-broker package."; }; d2bUnsafeLocalHelper = lib.mkOption { diff --git a/nixos-modules/options-ownership-matrix.nix b/nixos-modules/options-ownership-matrix.nix index 70ecc54b1c..919519e47f 100644 --- a/nixos-modules/options-ownership-matrix.nix +++ b/nixos-modules/options-ownership-matrix.nix @@ -120,7 +120,7 @@ let `recursive` is hard-pinned to false and the daemon-side enforcer additionally asserts the carve-out by name (see packages/d2b-host/src/ownership_matrix.rs). `required` is - false: native (post-cutover) VMs never had this artifact, so + false: native VMs never had this artifact, so its absence must not fail preflight; migrated VMs still have it checked/postured when present. ''; @@ -356,7 +356,7 @@ in lazily-created paths (`store-view/live/.d2b-marker-`, `store-view/state/integrity-unknown.json`) and for legacy recovery artifacts (`store`, `store-meta`) absent on - native post-cutover VMs. + native VMs. ''; }; recursive = mkOption { diff --git a/nixos-modules/options-realms-workloads.nix b/nixos-modules/options-realms-workloads.nix index 33dbf0133b..cb28b72f1d 100644 --- a/nixos-modules/options-realms-workloads.nix +++ b/nixos-modules/options-realms-workloads.nix @@ -186,7 +186,7 @@ let let workloadId = config.id; # stateDir default respects legacyVmName so existing on-disk state - # (TPM, store-view, guest-control token, …) is preserved when the + # (TPM, store-view, ComponentSession enrollment, …) is preserved when the # workload wraps a legacy d2b.vms entry. No activation-time migration # ever occurs; the path is purely a default override. defaultStateDir = @@ -259,7 +259,7 @@ let Primary state directory for this workload. When `legacyVmName` is set the default maps to `/var/lib/d2b/vms/`, preserving existing on-disk state (TPM, store-view, audio state, - guest-control token, …) without any activation-time migration. + ComponentSession enrollment, …) without any activation-time migration. When `legacyVmName` is null the default is `/var/lib/d2b/vms/`. It is null for `kind = "unsafe-local"` because the provider owns no host VM @@ -497,7 +497,7 @@ let Primary application/launch command for this workload. Emitted as the `Exec=` field in generated `.desktop` metadata. When null, d2b derives a default based on the - workload kind and available guest-control capabilities. + workload kind and available component-session capabilities. ''; }; diff --git a/nixos-modules/options-site.nix b/nixos-modules/options-site.nix index 305c309a80..7850e26860 100644 --- a/nixos-modules/options-site.nix +++ b/nixos-modules/options-site.nix @@ -60,7 +60,7 @@ default = true; description = '' Use release prebuilt host binaries for `d2b`, `d2bd`, - `d2b-priv-broker`, and the activation helper when they are + `d2b-broker`, and the activation helper when they are available. Set to `false` on development hosts that intentionally validate the checked-out flake's Rust sources before a release artifact exists. @@ -106,12 +106,12 @@ **Reserved.** The broker accepts `--audit-retention-days ` and the runtime prune-on-rotate - loop is shipping in `packages/d2b-priv-broker/src/audit.rs`, + loop is shipping in `packages/d2b-broker/src/audit.rs`, but the NixOS module does not yet spawn the broker (`d2bd` does so at runtime in a future wiring, and this option's value will then thread through - `daemon-config.json` → `d2bd` → `d2b-priv-broker - serve --audit-retention-days `). Until that wiring + `daemon-config.json` → `d2bd` → `d2b-broker + host --audit-retention-days `). Until that wiring lands, overriding this option is a no-op at runtime - the broker defaults to 14 days regardless. diff --git a/nixos-modules/options-vms.nix b/nixos-modules/options-vms.nix index f0c4eb8eb1..4b6394d75d 100644 --- a/nixos-modules/options-vms.nix +++ b/nixos-modules/options-vms.nix @@ -1078,124 +1078,15 @@ in }; }; - guest.control = { + guest.componentSession = { enable = lib.mkOption { type = lib.types.bool; default = false; description = '' - Enable the guest-control credential/share surface and the - `d2b-guestd` service wiring for this VM. The static - guest-control binaries are installed for every VM; this option - opts the VM into the live guest-control plane (credential share - plus guestd), which serves the readiness Health probe, `config - sync` reads (`ReadGuestFile`), and - when `guest.exec` is also - enabled - admin guest exec. All of it runs over the - authenticated guest-control vsock, not SSH. - ''; - }; - auth.tokenFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/run/secrets/d2b/work/guest-control-token"; - description = '' - Absolute runtime path to an operator-managed guest-control token - file. Do not use Nix path literals such as `./token`; those can - copy secret material into `/nix/store`. When null, d2b - generates a stable per-VM fallback token under - `d2b.site.stateDir` outside the runner-writable per-VM - state root. - - Runtime validation requires the source and its parent - directories to be symlink-free, the file to be regular, - root-owned, outside `/nix/store`, and inaccessible to - group/world permission bits. D2b materializes a - root-owned copy readable only by the dedicated - `d2b--gctlfs` guest-control virtiofsd principal. - ''; - }; - }; - - guest.exec = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Enable the guest-control exec runtime for this VM. - - This wires the guestd exec service so admin operators (callers in - `d2b.site.adminUsers`) can run `d2b vm exec` against this - VM over the authenticated guest-control vsock - no SSH. Exec is - off by default; enabling it requires `guest.control.enable = true` - and a workload user (`ssh.user`). - - Every exec runs the requested command as the VM's workload user - (`ssh.user`) - never as root - inside a real PAM login session - (`systemd-run --property=PAMName=login --uid=`), so the - command sees the same environment an SSH login would - (`XDG_RUNTIME_DIR`, the login-shell profile, …). Users elevate - with `sudo` inside the session. - ''; - }; - - # Tombstones (removed options): guest-control exec now always - # runs as the VM's workload user (`ssh.user`) in a PAM login - # session - never root - and there is no per-VM exec user - # allowlist. These hidden stubs keep legacy assignments landing - # on the friendly migration assertions in assertions.nix instead - # of a cryptic "option does not exist" module-system error. - allowRoot = lib.mkOption { - type = lib.types.bool; - default = false; - internal = true; - visible = false; - description = '' - REMOVED. Guest-control exec always runs as the VM's workload - user (`ssh.user`), never root. Elevate with `sudo` inside the - session. See the migration assertion in assertions.nix. - ''; - }; - - users = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - internal = true; - visible = false; - description = '' - REMOVED. Guest-control exec always targets the VM's single - workload user (`ssh.user`); there is no per-VM exec user - allowlist. See the migration assertion in assertions.nix. - ''; - }; - - detachedMaxRuntimeSec = lib.mkOption { - type = lib.types.ints.unsigned; - default = 0; - example = 86400; - description = '' - Default runtime ceiling, in seconds, for detached execs on this - VM. `0` (the default) means no ceiling: a detached exec may run - indefinitely until it exits or is cancelled. - - When non-zero, guestd passes the value to the per-exec transient - unit as a `RuntimeMaxSec` ceiling; a detached exec exceeding it is - terminated and reported as expired. This is a guest-enforced - backstop, not a substitute for explicit cancellation. - ''; - }; - - interactiveMaxRuntimeSec = lib.mkOption { - type = lib.types.ints.unsigned; - default = 0; - example = 28800; - description = '' - Default runtime ceiling, in seconds, for interactive (TTY) execs - on this VM. `0` (the default) means no ceiling: an interactive - session is connection-owned and may run indefinitely until it - exits or the controlling connection drops. - - This ceiling applies only to interactive `tty = true`, - non-detached execs. Non-interactive attached execs keep their - fixed built-in runtime ceiling regardless of this value. + Enable the Guest target agent and its enrolled + ComponentSession for this VM. The Guest evaluator starts + `d2bd guest` and the separate Guest-profile broker; it does not + create a local Zone store or public operator socket. ''; }; }; @@ -1205,15 +1096,11 @@ in type = lib.types.bool; default = false; description = '' - Enable the staged persistent guest-shell contract for this VM. - - Persistent shells use the authenticated guest-control plane and the - same workload-user terminal substrate as guest exec. The option is - default-off and requires `guest.control.enable = true`, - `guest.exec.enable = true`, and a non-root workload user - (`ssh.user`). Runtime shpool attachment is staged separately; this - option currently carries the host/guest policy and manifest - contract used by runtime implementations. + Enable the staged persistent ShellSession contract for this VM. + + Persistent shells use authenticated ComponentSession named streams + and the target-local Process terminal substrate. The option is + default-off and requires `guest.componentSession.enable = true`. ''; }; diff --git a/nixos-modules/prebuilt-packages.nix b/nixos-modules/prebuilt-packages.nix index f2798e96f4..ae95ce1507 100644 --- a/nixos-modules/prebuilt-packages.nix +++ b/nixos-modules/prebuilt-packages.nix @@ -1,3 +1,16 @@ { pkgs, lib }: -import ../nix/prebuilt.nix { inherit pkgs lib; } +let + packages = import ../nix/prebuilt.nix { inherit pkgs lib; }; + selectPackage = name: fallback: + let + candidate = + if packages != null && builtins.hasAttr name packages + then builtins.getAttr name packages + else null; + in + if candidate != null && (candidate.sourceBinary or null) == null + then candidate + else fallback; +in +(if packages == null then { } else packages) // { inherit selectPackage; } diff --git a/nixos-modules/privileges-json.nix b/nixos-modules/privileges-json.nix index f09ec93bfa..31eb7e45e7 100644 --- a/nixos-modules/privileges-json.nix +++ b/nixos-modules/privileges-json.nix @@ -284,18 +284,6 @@ let "brokerRequired": "no-mutation", "auditMode": "yes" }, - { - "operation": "host cutover", - "subject": "host cutover", - "scope": "global", - "allowedGroups": [ - "d2b-admin" - ], - "destructive": true, - "secretAccess": "metadata-only", - "brokerRequired": "yes", - "auditMode": "yes" - }, { "operation": "host prepare", "subject": "host", @@ -392,18 +380,6 @@ let "brokerRequired": "yes", "auditMode": "yes" }, - { - "operation": "host migrate-storage --apply", - "subject": "host/storage", - "scope": "global", - "allowedGroups": [ - "d2b-admin" - ], - "destructive": true, - "secretAccess": "possible-paths-only", - "brokerRequired": "yes", - "auditMode": "yes" - }, { "operation": "up", "subject": "VM/env", @@ -1338,18 +1314,6 @@ let "brokerRequired": "yes", "auditMode": "yes" }, - { - "operation": "GuestControlSign", - "subject": "guest-control token", - "scope": "per-VM", - "allowedGroups": [ - "d2bd" - ], - "destructive": false, - "secretAccess": "redacted-only", - "brokerRequired": "yes", - "auditMode": "yes" - }, { "operation": "InjectSecretById", "subject": "secret/key", @@ -1783,42 +1747,6 @@ let "brokerRequired": "yes", "auditMode": "yes" }, - { - "operation": "LaunchCutoverRunner", - "subject": "cutover-runner", - "scope": "global", - "allowedGroups": [ - "d2bd" - ], - "destructive": true, - "secretAccess": "metadata-only", - "brokerRequired": "yes", - "auditMode": "yes" - }, - { - "operation": "CutoverAudit", - "subject": "cutover-audit", - "scope": "global", - "allowedGroups": [ - "d2b-cutover-runner" - ], - "destructive": false, - "secretAccess": "metadata-only", - "brokerRequired": "yes", - "auditMode": "yes" - }, - { - "operation": "CutoverEffect", - "subject": "cutover-effect", - "scope": "global", - "allowedGroups": [ - "d2b-cutover-runner" - ], - "destructive": true, - "secretAccess": "metadata-only", - "brokerRequired": "yes", - "auditMode": "yes" - }, { "operation": "ApplyHostGenerationHandoff", "subject": "host-generation", diff --git a/nixos-modules/processes-json.nix b/nixos-modules/processes-json.nix index 1010f595c0..074c4330e7 100644 --- a/nixos-modules/processes-json.nix +++ b/nixos-modules/processes-json.nix @@ -40,6 +40,8 @@ let fallback = d2bWaylandProxySourcePackage; }; d2bWaylandProxyBinary = "${d2bWaylandProxyPackage}/bin/d2b-wayland-proxy"; + wlCrossDomainProxyPackage = import ../pkgs/wl-cross-domain-proxy { inherit pkgs; }; + wlCrossDomainProxyBinary = "${wlCrossDomainProxyPackage}/bin/wl-cross-domain-proxy"; backendPort = envName: cfg._index.usbip.backendPorts.${envName}; @@ -51,9 +53,7 @@ let vsockSocketForPort = socketPath: port: "${socketPath}_${toString port}"; shareNodeId = share: "virtiofsd-${clean share.tag}"; shareSocketPath = name: share: - if share.tag == "d2b-gctl" - then "/run/d2b/vms/${name}/guest-control/${clean share.tag}.sock" - else "/run/d2b/vms/${name}/${clean share.tag}.sock"; + "/run/d2b/vms/${name}/${clean share.tag}.sock"; volumeHostPath = name: volume: d2bLib.volumeHostPath cfg.store.stateDir name volume; mkReadiness = kind: value: { inherit kind value; }; @@ -63,13 +63,6 @@ let unixSocketListening = mkReadiness "unix-socket-listening"; tcpPort = host: port: mkReadiness "tcp-port" { inherit host port; }; commandReady = mkReadiness "command"; - # Authenticated guest-control Health readiness. Unlike a raw TCP-22 - # probe this predicate fails CLOSED: the daemon completes a full - # Hello + token challenge-response + Health over the guest-control vsock - # before the node is ready. The daemon resolves the per-VM vsock socket, - # peer credentials, and broker-backed signer from its own trusted state. - guestControlHealthReady = vmName: { kind = "guest-control-health"; value = { vm = vmName; }; }; - extractOptValues = optFlag: extraArgs: let flags = if builtins.isList optFlag then optFlag else [ optFlag ]; @@ -288,7 +281,7 @@ let supportsNotifySocket = true; vsockOpts = if hasUserVsockExtraArg then - throw "d2b.vms.${name}.config.microvm.cloud-hypervisor.extraArgs must not set --vsock; d2b owns the Cloud Hypervisor vsock device for guest control and observability" + throw "d2b.vms.${name}.config.microvm.cloud-hypervisor.extraArgs must not set --vsock; d2b owns the Cloud Hypervisor vsock device for ComponentSession and observability" else "cid=${toString vsockCID},socket=${vsockPath}"; virtiofsShares = lib.filter (share: (share.proto or "virtiofs") == "virtiofs") microvm.shares; @@ -695,6 +688,14 @@ let ] ++ denyArgs ++ allowArgs ++ maxVersionArgs ++ dmabufAllowArgs ++ dmabufDenyArgs; }; + waylandFrontendRunner = name: { + binaryPath = wlCrossDomainProxyBinary; + # The guest desktop exports WAYLAND_DISPLAY=wayland-1. Use the explicit + # socket-name mode because this Process runs as a system service and does + # not inherit the user's session environment. + argv = [ "d2b-${name}-wayland-frontend" "--socket-name" "wayland-1" ]; + }; + videoBinaryPath = _name: # the per-VM # `d2b-${name}-video.service` was deleted. The video @@ -884,7 +885,20 @@ use devices::virtio::vhost_user_backend::run_video_device;' ownerGid = ownerProfile.gid; }; - mkProcessNode = name: { id, role, readiness, unit ? null, binaryPath ? null, argv ? [ ], env ? [ ], planOps ? [ ], networkInterfaces ? [ ] }: + mkProcessNode = name: { + id, + role, + readiness, + unit ? null, + binaryPath ? null, + argv ? [ ], + env ? [ ], + planOps ? [ ], + networkInterfaces ? [ ], + executionRef ? null, + executionDomain ? null, + userRef ? null + }: let # `vm.supervisor` was removed per ADR 0015; every enabled VM remains # daemon-supervised. These unit names are only transient-effect @@ -897,6 +911,10 @@ use devices::virtio::vhost_user_backend::run_video_device;' assert (binaryPath == null) == (argv == [ ]); { inherit id role readiness; + executionRef = + if role == "activation-nixos-runner" + then "Guest/${name}" + else "Host/host-system"; profile = profileFor name id; } // lib.optionalAttrs emitUnit { inherit unit; } @@ -911,6 +929,15 @@ use devices::virtio::vhost_user_backend::run_video_device;' } // lib.optionalAttrs (networkInterfaces != [ ]) { inherit networkInterfaces; + } + // lib.optionalAttrs (executionRef != null) { + inherit executionRef; + } + // lib.optionalAttrs (executionDomain != null) { + inherit executionDomain; + } + // lib.optionalAttrs (userRef != null) { + inherit userRef; }; mkRunnerNode = name: args: runner: @@ -941,11 +968,17 @@ use devices::virtio::vhost_user_backend::run_video_device;' manifest = cfg.manifest.${name}; microvm = d2bLib.vmRunner config name; hypervisorService = d2bLib.runtimeHypervisorService "nixos"; - # The guest-control authenticated Health probe is the framework - # readiness gate on guest-control-capable VMs. Per-VM sshd/host-keys are - # retained for the SSH-compat window but are no longer the framework - # readiness signal, so a TCP-22 readiness node is no longer emitted. - guestControlEnabled = vm.guest.control.enable; + # Guest sessions are authenticated independently from VM boot. Their + # evidence is projected through Guest/Endpoint status rather than a + # readiness-only health RPC. + componentSessionEnabled = vm.guest.componentSession.enable; + activationRunner = name: { + binaryPath = "${d2bHostTools.activationHelper}/bin/d2b-activation-helper"; + argv = [ + "d2b-activation-helper" + "apply-generation" + ]; + }; virtiofsShares = lib.filter (share: (share.proto or "virtiofs") == "virtiofs") microvm.shares; @@ -1059,10 +1092,19 @@ use devices::virtio::vhost_user_backend::run_video_device;' ++ lib.optional emitWaylandProxy (mkRunnerNode name { id = "wayland-proxy"; role = "wayland-proxy"; + executionRef = "Host/host-system"; + executionDomain = "system"; readiness = [ (unixSocketListening "/run/d2b-wlproxy/${name}/wayland-0") ]; } (waylandProxyRunner "local-vm" name vm)) + ++ lib.optional emitWaylandProxy (mkRunnerNode name { + id = "wayland-frontend-worker"; + role = "wayland-proxy"; + executionRef = "Guest/${name}"; + executionDomain = "system"; + readiness = [ ]; + } (waylandFrontendRunner name)) ++ lib.optional vm.audio.enable (mkRunnerNode name { id = "audio"; role = "audio"; @@ -1125,11 +1167,11 @@ use devices::virtio::vhost_user_backend::run_video_device;' readiness = [ (unixSocketExists "/run/d2b/otel/host-egress.sock") ]; runner = otelHostBridgeRunner manifest; }) - ++ lib.optional guestControlEnabled (node name { - id = "guest-control-health"; - role = "guest-control-health"; - readiness = [ (guestControlHealthReady name) ]; - }) + ++ lib.optional componentSessionEnabled (mkRunnerNode name { + id = "activation-nixos-runner"; + role = "activation-nixos-runner"; + readiness = [ ]; + } (activationRunner name)) ++ lib.optional vm.usb.securityKey.enable (node name { # The sk-frontend DAG node tracks the readiness of the host-side # vsock socket endpoint that the guest frontend connects to. The @@ -1176,10 +1218,9 @@ use devices::virtio::vhost_user_backend::run_video_device;' edgesFromNodes optionalSidecarBaseNodeIds "audio" "The audio sidecar starts only after every prerequisite sidecar is ready." ) ++ edgesFromNodes preVmmNodeIds "cloud-hypervisor" "Cloud Hypervisor starts only after every prerequisite sidecar is ready." - ++ lib.optional guestControlEnabled - (edge "cloud-hypervisor" "guest-control-health" "Authenticated guest-control Health readiness is probed only after Cloud Hypervisor is running.") ++ lib.optional vm.usb.securityKey.enable - (edge "cloud-hypervisor" "sk-frontend" "The sk-frontend vsock endpoint tracking starts only after Cloud Hypervisor creates the base vsock socket."); + (edge "cloud-hypervisor" "sk-frontend" "The sk-frontend vsock endpoint tracking starts only after Cloud Hypervisor creates the base vsock socket.") + ; invariants = { perVmAuditPipeline = true; swtpmPreStartFlush = true; @@ -1289,14 +1330,63 @@ use devices::virtio::vhost_user_backend::run_video_device;' (lib.mapAttrsToList vmDag normalNixosVms) ++ (lib.mapAttrsToList qemuMediaDag qemuMediaVms) ++ (lib.mapAttrsToList usbipdDag cfg._index.usbip.envMeta); - } // lib.optionalAttrs (config.d2b._bundle.cutoverRunnerPath != null) { - cutoverRunner = { - binaryPath = config.d2b._bundle.cutoverRunnerPath; - persistent = false; - cgroupPlacement = "outside-d2b.slice"; - singleBootstrapFd = true; + }; + + guestProcessResourceRows = lib.filter + (row: + builtins.elem row.resource.type [ "Process" "EphemeralProcess" ] + && builtins.isString (row.spec.executionRef or null) + && lib.hasPrefix "Guest/" row.spec.executionRef) + (cfg._resourceCompiler.processes.rows or [ ]); + guestProcessRefs = lib.unique ( + (map + (row: row.spec.executionRef) + guestProcessResourceRows) + ++ (lib.concatLists (map + (dag: + lib.optional + (lib.any (node: (node.executionRef or null) == "Guest/${dag.vm}") dag.nodes) + "Guest/${dag.vm}") + data.vms))); + guestProcessInputsFor = guestRef: + let + resourceRows = lib.filter + (row: row.spec.executionRef == guestRef) + guestProcessResourceRows; + dagNodes = lib.concatLists (map + (dag: + if guestRef == "Guest/${dag.vm}" + then lib.filter + (node: (node.executionRef or null) == guestRef) + dag.nodes + else [ ]) + data.vms); + resources = map + (row: { + apiVersion = "resources.d2bus.org/v3"; + type = row.resource.type; + metadata = { + name = row.resourceName; + zone = row.zoneName; + }; + spec = row.spec; + }) + resourceRows; + in + { + guest = guestRef; + nodes = dagNodes; + resources = resources; }; + guestProcessInputsData = { + schemaVersion = "v1"; + guests = map guestProcessInputsFor + (lib.sort lib.lessThan guestProcessRefs); }; + guestProcessInputsJson = builtins.toJSON guestProcessInputsData; + guestProcessInputsPath = pkgs.writeText + "d2b-guest-process-inputs.json" + guestProcessInputsJson; jsonText = builtins.toJSON data; jsonFile = pkgs.writeText "d2b-processes.json" jsonText; @@ -1338,5 +1428,13 @@ in classification = "contractPrivateNonSecret"; sensitivity = "nonSecret"; }; + d2b._bundle.extraArtifacts.guestProcessInputs = { + data = guestProcessInputsData; + jsonText = guestProcessInputsJson; + path = guestProcessInputsPath; + installFileName = "guest-process-inputs.json"; + classification = "contractPrivateNonSecret"; + sensitivity = "nonSecret"; + }; }; } diff --git a/nixos-modules/provider-catalog.nix b/nixos-modules/provider-catalog.nix index d30a259c8a..b033d086cc 100644 --- a/nixos-modules/provider-catalog.nix +++ b/nixos-modules/provider-catalog.nix @@ -41,6 +41,9 @@ let # shape is pinned here rather than left free-form because exact-digest # selection compares these values literally. digestPattern = "sha256:[0-9a-f]{64}"; + signedContractFields = shape.placementContractFields + ++ shape.runtimeContractFields; + catalogFields = shape.fields ++ signedContractFields; artifactModule = types.submodule ({ name, config, ... }: { options = { @@ -104,7 +107,7 @@ let entry = if artifact.catalog == null then { } - else lib.filterAttrs (fieldName: _: lib.elem fieldName shape.fields) artifact.catalog; + else lib.filterAttrs (fieldName: _: lib.elem fieldName catalogFields) artifact.catalog; }) artifactIds; @@ -148,7 +151,7 @@ let unknownFields = id: if artifacts.${id}.catalog == null then [ ] - else lib.filter (field: !(lib.elem field shape.fields)) + else lib.filter (field: !(lib.elem field catalogFields)) (lib.attrNames artifacts.${id}.catalog); badDigests = id: @@ -160,6 +163,163 @@ let in value == null || builtins.match digestPattern (toString value) == null) shape.digestFields; + targetKinds = [ "zone" "host" "guest" ]; + effectClasses = [ + "runtime" + "transport" + "substrate" + "process" + "volume" + "storage" + "network" + "device" + "display" + "audio" + "credential" + "observability" + ]; + placementScopes = [ + "zone-singleton" + "fixed-execution-target" + "per-resource-target" + ]; + placementAnchors = [ "zone" "execution-ref" ]; + targetCapabilityKeys = [ + "artifactDigest" + "requiredEffectClasses" + "targetKind" + ]; + validContractDigest = value: + builtins.isString value && builtins.match digestPattern value != null; + + signedContractAssertions = id: + let + catalog = + if builtins.isAttrs artifacts.${id}.catalog + then artifacts.${id}.catalog + else { }; + placementPresent = lib.any + (field: builtins.hasAttr field catalog) + shape.placementContractFields; + runtimePresent = lib.any + (field: builtins.hasAttr field catalog) + shape.runtimeContractFields; + scope = catalog.instanceScope or null; + supported = + if builtins.isList (catalog.supportedTargetKinds or null) + then catalog.supportedTargetKinds + else [ ]; + capabilities = + if builtins.isList (catalog.targetCapabilities or null) + then catalog.targetCapabilities + else [ ]; + capabilityTargets = map + (capability: + if builtins.isAttrs capability + then capability.targetKind or null + else null) + capabilities; + capabilityValid = capability: + let + effects = + if builtins.isAttrs capability + && builtins.isList (capability.requiredEffectClasses or null) + then capability.requiredEffectClasses + else [ ]; + in + builtins.isAttrs capability + && lib.sort lib.lessThan (lib.attrNames capability) == targetCapabilityKeys + && builtins.elem (capability.targetKind or null) targetKinds + && validContractDigest (capability.artifactDigest or null) + && builtins.isList (capability.requiredEffectClasses or null) + && lib.length (lib.unique effects) == lib.length effects + && lib.all (class: builtins.elem class effectClasses) effects; + scopeShape = + if scope == "zone-singleton" + then supported == [ "zone" ] && capabilities != [ ] + else if scope == "fixed-execution-target" + then lib.length supported == 1 && !(builtins.elem "zone" supported) + else if scope == "per-resource-target" + then supported != [ ] && !(builtins.elem "zone" supported) + else false; + in + (lib.optionals placementPresent [ + { + assertion = lib.all + (field: builtins.hasAttr field catalog) + shape.placementContractFields; + message = '' + d2b.artifacts."${id}".catalog must carry the complete signed + controller placement contract (${lib.concatStringsSep ", " shape.placementContractFields}). + ''; + } + { + assertion = builtins.elem scope placementScopes; + message = '' + d2b.artifacts."${id}".catalog.instanceScope must be one of + ${lib.concatStringsSep ", " placementScopes}. + ''; + } + { + assertion = builtins.isList supported + && supported != [ ] + && builtins.all builtins.isString supported + && lib.length (lib.unique supported) == lib.length supported + && lib.sort lib.lessThan supported == supported + && lib.all (target: builtins.elem target targetKinds) supported; + message = '' + d2b.artifacts."${id}".catalog.supportedTargetKinds must be a + sorted, unique, non-empty subset of zone, host, and guest. + ''; + } + { + assertion = builtins.isList capabilities + && capabilities != [ ] + && lib.length capabilities <= 3 + && builtins.all builtins.isAttrs capabilities + && builtins.all builtins.isString capabilityTargets + && lib.length (lib.unique capabilityTargets) == lib.length capabilityTargets + && lib.sort lib.lessThan capabilityTargets == supported + && lib.all capabilityValid capabilities; + message = '' + d2b.artifacts."${id}".catalog.targetCapabilities must provide one + complete signed capability for every supported target kind. + ''; + } + { + assertion = builtins.elem (catalog.placementAnchor or null) placementAnchors; + message = '' + d2b.artifacts."${id}".catalog.placementAnchor must be zone or execution-ref. + ''; + } + { + assertion = scopeShape; + message = '' + d2b.artifacts."${id}".catalog signed placement scope is incompatible + with its supported target kinds. + ''; + } + ]) + ++ (lib.optionals runtimePresent [ + { + assertion = lib.all + (field: builtins.hasAttr field catalog) + shape.runtimeContractFields; + message = '' + d2b.artifacts."${id}".catalog must carry both signed shared runtime + artifact digests (${lib.concatStringsSep ", " shape.runtimeContractFields}). + ''; + } + { + assertion = validContractDigest (catalog.d2bdDigest or null) + && validContractDigest (catalog.brokerDigest or null); + message = '' + d2b.artifacts."${id}".catalog.d2bdDigest and brokerDigest must be + lowercase sha256 digests. + ''; + } + ]); + maxOutputNameLength = 16; maxOutputNamesInMessage = 4; @@ -298,7 +458,7 @@ in }) artifactIds) - # No field outside the frozen set. + # No field outside the generated catalog or signed runtime contract sets. ++ (map (id: { assertion = unknownFields id == [ ]; @@ -326,5 +486,10 @@ in # Provider packages must name one determinate output before any later # required-output validation can diagnose the artifact layout. - ++ (map providerOutputSelection artifactIds); + ++ (map providerOutputSelection artifactIds) + + # Placement and shared-runtime fields are optional until a Provider + # package publishes its signed manifest, but once any is present the + # complete closed contract is validated. + ++ (lib.concatMap signedContractAssertions artifactIds); } diff --git a/nixos-modules/realm-controller-config-json.nix b/nixos-modules/realm-controller-config-json.nix index 79396d8f99..bc82d5e2fc 100644 --- a/nixos-modules/realm-controller-config-json.nix +++ b/nixos-modules/realm-controller-config-json.nix @@ -68,7 +68,7 @@ let stateDir = cfg.manifest.${vmName}.stateDir; runDir = "/run/d2b/vms/${vmName}"; storeView = "${toString cfg.store.stateDir}/${vmName}/store-view"; - guestControlDir = "/run/d2b/vms/${vmName}/guest-control"; + componentSessionDir = "/run/d2b/vms/${vmName}/component-session"; }; }; # Realm-native workload identity: present for explicit realm workload diff --git a/nixos-modules/resources-zones-processes.nix b/nixos-modules/resources-zones-processes.nix index a9fa786dc1..23f1b5fe26 100644 --- a/nixos-modules/resources-zones-processes.nix +++ b/nixos-modules/resources-zones-processes.nix @@ -286,9 +286,9 @@ let Audio = "Provider/audio-pipewire"; CloudHypervisorRunner = "Provider/runtime-cloud-hypervisor"; QemuMediaRunner = "Provider/runtime-qemu-media"; + ActivationNixosRunner = "Provider/activation-nixos"; VsockRelay = "Provider/transport-vsock"; OtelHostBridge = "Provider/observability-otel"; - GuestControlHealth = "Provider/system-core"; Usbip = "Provider/device-usbip"; SecurityKeyFrontend = "Provider/device-security-key"; WaylandProxy = "Provider/display-wayland"; diff --git a/nixos-modules/rust-host-tools.nix b/nixos-modules/rust-host-tools.nix index 94c8856ca6..e7c7e0427a 100644 --- a/nixos-modules/rust-host-tools.nix +++ b/nixos-modules/rust-host-tools.nix @@ -117,7 +117,6 @@ let hostPackages = [ "d2bd" "d2b" - "d2b-cutover" "d2b-host" "d2b-host-activation-helper" "d2b-gateway-runtime" @@ -176,7 +175,7 @@ let }); brokerCargoArtifacts = craneLib.buildDepsOnly (commonBuildArgs // { - pname = "d2b-priv-broker"; + pname = "d2b-broker"; version = "0.0.0-bootstrap"; dummySrc = dummySource; sourceRoot = "d2b-provider-rust-src"; @@ -184,8 +183,8 @@ let cargoLock = brokerCargoLock; cargoVendorDir = brokerCargoVendorDir; inherit outputHashes; - cargoCheckExtraArgs = "--package d2b-priv-broker --no-default-features"; - cargoBuildExtraArgs = "--package d2b-priv-broker --no-default-features"; + cargoCheckExtraArgs = "--package d2b-broker --no-default-features"; + cargoBuildExtraArgs = "--package d2b-broker --no-default-features"; }); installBinaries = binaries: @@ -214,7 +213,7 @@ let }); broker = craneLib.buildPackage (commonBuildArgs // { - pname = "d2b-priv-broker"; + pname = "d2b-broker"; version = "0.0.0-bootstrap"; cargoArtifacts = brokerCargoArtifacts; inherit outputHashes; @@ -224,7 +223,7 @@ let cargoLock = brokerCargoLock; cargoVendorDir = brokerCargoVendorDir; cargoBuildExtraArgs = "--no-default-features"; - installPhaseCommand = installBinaries [ "d2b-priv-broker" ]; + installPhaseCommand = installBinaries [ "d2b-broker" ]; }); in { @@ -242,10 +241,6 @@ in package = "d2b-host"; binaries = [ "d2b-activation-helper" ]; }; - cutoverRunner = mkMainPackage { - package = "d2b-cutover"; - binaries = [ "d2b-cutover-runner" ]; - }; hostActivationHelper = mkMainPackage { package = "d2b-host-activation-helper"; binaries = [ "d2b-host-activation-helper" ]; diff --git a/nixos-modules/vm-evaluator.nix b/nixos-modules/vm-evaluator.nix index c3da0d1a97..551db3fa57 100644 --- a/nixos-modules/vm-evaluator.nix +++ b/nixos-modules/vm-evaluator.nix @@ -22,10 +22,17 @@ # networking, services, etc. - driven by the consumer's # `vm.config` module list). { inputs }: -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, d2bHostTools ? null, d2bHostToolOverrides ? null, ... }: let cfg = config.d2b; + guestHostTools = + if d2bHostToolOverrides == null + then d2bHostTools + else d2bHostTools // { + broker = d2bHostToolOverrides.broker; + d2bd = d2bHostToolOverrides.d2bd; + }; # Build a per-VM NixOS evaluation using the host's nixpkgs path. # `nixos/lib/eval-config.nix` is the standard NixOS eval entrypoint - @@ -48,7 +55,8 @@ let modules = [ ./vm-options.nix ./vm-guest-base.nix - ./guest-control.nix + ./component-session.nix + ./guest-broker.nix # Inherit host nixpkgs policy so per-VM evals honor the consumer's # allowUnfree / overlays / security fixes without re-stating them in # each per-VM module. @@ -61,7 +69,11 @@ let specialArgs = { inherit inputs; } // cfg.site.extraSpecialArgs - // { d2bInputs = inputs; }; + // { + d2bInputs = inputs; + d2bHostTools = guestHostTools; + d2bUsePrebuiltHostTools = cfg.site.usePrebuiltHostTools; + }; inherit (pkgs.stdenv.hostPlatform) system; }; diff --git a/nixos-modules/vm-guest-base.nix b/nixos-modules/vm-guest-base.nix index 3fc4be1b6e..af974397e8 100644 --- a/nixos-modules/vm-guest-base.nix +++ b/nixos-modules/vm-guest-base.nix @@ -145,7 +145,7 @@ in # /dev/disk/by-id/virtio-rootfs. Mount it at the overlay path # so the upperdir/workdir live on a real filesystem (ext4 - the # broker's DiskInit op runs mkfs.ext4 when creating the image; - # see packages/d2b-priv-broker/src/ops/disk_init.rs). + # see packages/d2b-broker/src/ops/disk_init.rs). # Without this mount the overlay upper/work live on the rootfs # tmpfs and are wiped on every reboot, which defeats the # writableStoreOverlay design. diff --git a/nixos-modules/vm-submodule.nix b/nixos-modules/vm-submodule.nix index 56ef212254..4b1c7a7e87 100644 --- a/nixos-modules/vm-submodule.nix +++ b/nixos-modules/vm-submodule.nix @@ -9,11 +9,11 @@ # # No upstream microvm.nix dependency anywhere in this graph. { inputs }: -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, d2bHostTools ? null, d2bHostToolOverrides ? null, ... }: let evaluator = (import ./vm-evaluator.nix { inherit inputs; }) - { inherit config lib pkgs; }; + { inherit config lib pkgs d2bHostTools d2bHostToolOverrides; }; in { _composeVm = evaluator._composeVm; diff --git a/packages/Cargo.guest.lock b/packages/Cargo.guest.lock index 3e63ac4e0a..851f59de76 100644 --- a/packages/Cargo.guest.lock +++ b/packages/Cargo.guest.lock @@ -2,21 +2,247 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -36,26 +262,118 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "azure_core" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e41cbd819986ba41904c207d8ffc4106f8f8352a548d773e9554906379bb2fb" +dependencies = [ + "async-lock", + "async-trait", + "azure_core_macros", + "bytes", + "futures", + "pin-project", + "rustc_version", + "serde", + "serde_json", + "tokio", + "tracing", + "typespec", + "typespec_client_core", +] + +[[package]] +name = "azure_core_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b52dba6a345f3ad2d42ff8d0d63df9d0994cfa29657bf18ffdbf149f78a4f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "tracing", +] + +[[package]] +name = "azure_identity" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32edf96b356ca7c51d7590c4925cc36efc3947a5da4468e8e0b25c56ecbb3de5" +dependencies = [ + "async-lock", + "async-trait", + "azure_core", + "futures", + "pin-project", + "serde", + "serde_json", + "time", + "tracing", + "url", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -64,9 +382,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] [[package]] name = "block-buffer" @@ -77,6 +404,34 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "byteorder" version = "1.5.0" @@ -85,9 +440,21 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] [[package]] name = "cfg-if" @@ -97,1024 +464,4489 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "chacha20" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ - "libc", + "cfg-if", + "cipher", + "cpufeatures 0.2.17", ] [[package]] -name = "crossbeam" -version = "0.8.4" +name = "chacha20" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "chacha20poly1305" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "crossbeam-utils", + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", ] [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "crypto-common", + "inout", + "zeroize", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.20" +name = "clap" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ - "crossbeam-utils", + "clap_builder", + "clap_derive", ] [[package]] -name = "crossbeam-queue" -version = "0.3.12" +name = "clap_builder" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ - "crossbeam-utils", + "anstream", + "anstyle", + "clap_lex", + "strsim", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "clap_derive" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "clap_lex" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] -name = "d2b-contracts" -version = "0.0.0-bootstrap" +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "async-trait", - "protobuf", - "schemars", - "semver", - "serde", - "serde_json", - "sha2", - "unicode-normalization", + "cc", ] [[package]] -name = "d2b-contracts-control" -version = "0.0.0-bootstrap" -dependencies = [ +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "command-fds" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f" +dependencies = [ + "nix 0.31.3", + "thiserror 2.0.20", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "d2b-audit" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-telemetry", + "libc", + "rustix 0.38.44", + "serde", + "serde_json", +] + +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + +[[package]] +name = "d2b-bus" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "d2b-audit", + "d2b-contracts", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-controller-toolkit", + "d2b-core-controller", + "d2b-resource-api", + "d2b-resource-store", + "d2b-resource-store-redb", + "d2b-session", + "d2b-session-unix", + "d2b-telemetry", + "getrandom 0.3.4", + "protobuf", + "quote", + "serde_json", + "sha2", + "snow", + "syn 2.0.119", + "tempfile", + "tokio", + "ttrpc", +] + +[[package]] +name = "d2b-contracts" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "protobuf", + "schemars", + "semver", + "serde", + "serde_json", + "sha2", + "unicode-normalization", +] + +[[package]] +name = "d2b-contracts-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-resource", + "schemars", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-contracts-control" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-resource", + "protobuf", + "schemars", + "serde", + "serde_json", +] + +[[package]] +name = "d2b-contracts-provider" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", "d2b-contracts", "d2b-contracts-resource", + "schemars", + "serde", + "serde_json", + "sha2", + "unicode-normalization", +] + +[[package]] +name = "d2b-contracts-resource" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", "protobuf", + "regex", + "schemars", + "serde", + "serde_json", + "sha2", + "unicode-normalization", +] + +[[package]] +name = "d2b-contracts-zone-session" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-provider", + "d2b-contracts-resource", "schemars", "serde", "serde_json", + "sha2", +] + +[[package]] +name = "d2b-controller-toolkit" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-bus", + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-process", + "d2b-process-conformance", + "d2b-provider-supervisor", + "d2b-provider-system-minijail", + "d2b-resource-api", + "d2b-resource-store", + "d2b-resource-store-redb", + "schemars", + "serde", + "serde_json", + "tempfile", + "tokio", +] + +[[package]] +name = "d2b-core" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-realm-core", + "rustix 0.38.44", + "schemars", + "semver", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-core-controller" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-controller-toolkit", + "d2b-telemetry", + "serde", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "d2b-gateway" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "d2b-realm-core", + "hmac", + "serde", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "d2b-gateway-runtime" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "azure_core", + "base64", + "chacha20poly1305", + "d2b-gateway", + "d2b-provider-runtime-azure-container-apps", + "d2b-provider-transport-azure-relay", + "d2b-realm-core", + "futures-util", + "getrandom 0.2.17", + "libc", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "webpki-roots 0.26.11", +] + +[[package]] +name = "d2b-host" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "libc", + "miniz_oxide", + "nix 0.29.0", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "ttrpc", +] + +[[package]] +name = "d2b-process" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-process-conformance", +] + +[[package]] +name = "d2b-process-conformance" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-provider" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-zone-routing", + "tokio", +] + +[[package]] +name = "d2b-provider-activation-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-broker", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-provider-audio-pipewire" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "libc", + "nix 0.29.0", + "schemars", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "d2b-provider-clipboard-wayland" +version = "0.0.0-bootstrap" +dependencies = [ + "command-fds", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-provider-toolkit", + "d2b-realm-core", + "env_logger", + "html2text", + "log", + "nix 0.29.0", + "notify-rust", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.20", + "wayland-client", + "wayland-protocols", + "wayland-protocols-misc", + "wayland-protocols-wlr", +] + +[[package]] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "libc", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "ttrpc", +] + +[[package]] +name = "d2b-provider-device-security-key" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "d2b-provider-device-tpm" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "serde", + "serde_json", +] + +[[package]] +name = "d2b-provider-device-usbip" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "libc", + "serde", + "serde_json", +] + +[[package]] +name = "d2b-provider-display-wayland" +version = "0.0.0-bootstrap" +dependencies = [ + "clap", + "d2b-contracts", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-provider-toolkit", + "d2b-realm-core", + "d2b-session", + "env_logger", + "getrandom 0.2.17", + "libc", + "log", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.20", + "uapi", + "wl-proxy", +] + +[[package]] +name = "d2b-provider-network-local" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "serde", + "serde_json", + "sha2", + "tempfile", +] + +[[package]] +name = "d2b-provider-notification-desktop" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-provider-toolkit", + "getrandom 0.2.17", + "schemars", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-provider-observability-otel" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-provider-toolkit", + "rustix 0.38.44", + "serde", + "serde_json", +] + +[[package]] +name = "d2b-provider-runtime-azure-container-apps" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "azure_core", + "azure_identity", + "d2b-contracts", + "d2b-realm-core", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "tokio", + "tracing", +] + +[[package]] +name = "d2b-provider-runtime-cloud-hypervisor" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-process-conformance", + "serde", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "d2b-provider-shell-terminal" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-process-conformance", +] + +[[package]] +name = "d2b-provider-supervisor" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-process", + "d2b-process-conformance", + "d2b-provider-system-minijail", + "d2b-provider-system-systemd", + "d2b-telemetry", + "rustix 0.38.44", + "serde_json", + "sha2", + "socket2 0.5.10", + "tempfile", + "tokio", +] + +[[package]] +name = "d2b-provider-system-core" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-provider-system-minijail" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-process-conformance", + "serde", +] + +[[package]] +name = "d2b-provider-system-systemd" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-process-conformance", + "d2b-provider-toolkit", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "d2b-provider-toolkit" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-provider", + "d2b-session", + "d2b-session-unix", + "schemars", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "d2b-provider-transport-azure-relay" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts", + "hmac", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "urlencoding", + "zeroize", +] + +[[package]] +name = "d2b-provider-volume-local" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-realm-core" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "schemars", + "serde", +] + +[[package]] +name = "d2b-resource-api" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core-controller", + "d2b-resource-store", + "d2b-resource-store-redb", + "d2b-telemetry", + "protobuf", + "serde_json", + "sha2", + "tempfile", + "tokio", + "ttrpc", +] + +[[package]] +name = "d2b-resource-store" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts-resource", +] + +[[package]] +name = "d2b-resource-store-redb" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-controller-toolkit", + "d2b-resource-store", + "d2b-telemetry", + "redb", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", +] + +[[package]] +name = "d2b-session" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "d2b-audit", + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-resource-api", + "d2b-telemetry", + "futures-util", + "protobuf", + "serde_json", + "sha2", + "snow", + "tokio", + "ttrpc", + "zeroize", +] + +[[package]] +name = "d2b-session-unix" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-session", + "envmnt", + "libc", + "listenfd", + "nix 0.29.0", + "rustix 0.38.44", + "tokio", + "tokio-vsock", +] + +[[package]] +name = "d2b-sk-frontend" +version = "0.0.0-bootstrap" +dependencies = [ + "libc", + "tokio", + "tokio-vsock", +] + +[[package]] +name = "d2b-telemetry" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "d2b-zone-routing" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-contracts", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-realm-core", +] + +[[package]] +name = "d2bd" +version = "0.0.0-bootstrap" +dependencies = [ + "arc-swap", + "async-trait", + "clap", + "d2b-audit", + "d2b-bus", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-core-controller", + "d2b-gateway", + "d2b-gateway-runtime", + "d2b-host", + "d2b-process", + "d2b-process-conformance", + "d2b-provider", + "d2b-provider-activation-nixos", + "d2b-provider-audio-pipewire", + "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", + "d2b-provider-device-security-key", + "d2b-provider-device-tpm", + "d2b-provider-device-usbip", + "d2b-provider-display-wayland", + "d2b-provider-network-local", + "d2b-provider-notification-desktop", + "d2b-provider-observability-otel", + "d2b-provider-runtime-azure-container-apps", + "d2b-provider-runtime-cloud-hypervisor", + "d2b-provider-shell-terminal", + "d2b-provider-supervisor", + "d2b-provider-system-core", + "d2b-provider-system-minijail", + "d2b-provider-system-systemd", + "d2b-provider-transport-azure-relay", + "d2b-provider-volume-local", + "d2b-realm-core", + "d2b-resource-api", + "d2b-resource-store", + "d2b-resource-store-redb", + "d2b-session", + "d2b-session-unix", + "d2bd-runtime", + "getrandom 0.2.17", + "libc", + "nix 0.29.0", + "notify-rust", + "parking_lot", + "protobuf", + "regex", + "rustix 0.38.44", + "semver", + "serde", + "serde_json", + "sha2", + "socket2 0.5.10", + "subtle", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "ttrpc", + ] + +[[package]] +name = "d2bd-runtime" +version = "0.0.0-bootstrap" +dependencies = [ + "arc-swap", + "async-trait", + "d2b-audit", + "d2b-bus", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-provider", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-core-controller", + "d2b-host", + "d2b-realm-core", + "d2b-resource-api", + "d2b-resource-store", + "d2b-resource-store-redb", + "d2b-session", + "d2b-session-unix", + "getrandom 0.2.17", + "libc", + "nix 0.29.0", + "parking_lot", + "protobuf", + "regex", + "rustix 0.38.44", + "semver", + "serde", + "serde_json", + "sha2", + "socket2 0.5.10", + "subtle", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "ttrpc", + "uzers", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "debug-fn" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24efe21bd9a78102d1225f10f0a41d9d5b43f4df7ae8235f39a9c79e4d476c1e" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "envmnt" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73999a2b8871e74c8b8bc23759ee9f3d85011b24fafc91a4b3b5c8cc8185501" +dependencies = [ + "fsio", + "indexmap 1.9.3", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error_reporter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8" + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsio" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4944f16eb6a05b4b2b79986b4786867bb275f52882adea798f17cc2588f25b2" +dependencies = [ + "dunce", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html2text" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389acaa5a86952dcdc88a43bc18021009cb9885b0ff8c2fff1cc44e4f34c2fae" +dependencies = [ + "html5ever", + "tendril", + "thiserror 2.0.20", + "unicode-width", +] + +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "isnt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1697e6b71679da96d5c41bb9035116141baadbf59a60625fd66cb3c9584e7b0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linearize" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c733520a3e6c2c5ff9933f88e5bbde446838d4fa65d357ce9b20eb4d264dcd9" +dependencies = [ + "cfg-if", + "linearize-derive", + "rand 0.10.2", + "rand 0.8.7", + "rand 0.9.5", + "version_check", +] + +[[package]] +name = "linearize-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657db73fbcad5341c5991ddee6c464d4bfd521575c0dc1a47913e0f434defeb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "listenfd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87bc54a4629b4294d0b3ef041b64c40c611097a677d9dc07b2c67739fe39dba" +dependencies = [ + "libc", + "uuid", + "winapi", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2", + "objc2-foundation", + "time", + "uuid", +] + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "notify-rust" +version = "4.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.7", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.5", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.5", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redb" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e925444704b5f17d32bf42f5b6e2df050bceebc3dcd6e71cc73dafe8092e839" +dependencies = [ + "libc", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.9", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "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", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "run-on-drop" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c9afdcca45aed5b357d64e0d033f6400c07a668aa4f9b92d25d39a884889457" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "itoa", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "snow" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "getrandom 0.3.4", + "ring", + "rustc_version", + "sha2", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "d2b-contracts-resource" -version = "0.0.0-bootstrap" +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "d2b-contracts", - "protobuf", - "regex", - "schemars", - "serde", - "serde_json", - "sha2", - "unicode-normalization", + "futures-core", ] [[package]] -name = "d2b-core" -version = "0.0.0-bootstrap" +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "d2b-contracts", - "d2b-realm-core", - "rustix 0.38.44", - "schemars", - "semver", - "serde", - "serde_json", - "sha2", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "d2b-exec-runner" -version = "0.0.0-bootstrap" +name = "tauri-winrt-notification" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" dependencies = [ - "rustix 0.38.44", + "thiserror 2.0.20", + "windows", + "windows-version", ] [[package]] -name = "d2b-guestd" -version = "0.0.0-bootstrap" +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "async-trait", - "d2b-contracts", - "d2b-contracts-control", - "d2b-exec-runner", - "d2b-realm-core", - "futures", - "hmac", - "protobuf", - "rustix 0.38.44", - "sha2", - "tokio", - "tokio-vsock", - "ttrpc", + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] -name = "d2b-realm-core" -version = "0.0.0-bootstrap" +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ - "d2b-contracts", - "schemars", - "serde", + "futf", + "mac", + "utf-8", ] [[package]] -name = "d2b-sk-frontend" -version = "0.0.0-bootstrap" +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "libc", - "tokio", - "tokio-vsock", + "thiserror-impl 1.0.69", ] [[package]] -name = "digest" -version = "0.10.7" +name = "thiserror" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "thiserror-impl 2.0.20", ] [[package]] -name = "dyn-clone" -version = "1.0.20" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] -name = "either" -version = "1.16.0" +name = "thiserror-impl" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] [[package]] -name = "equivalent" -version = "1.0.2" +name = "thread_local" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] [[package]] -name = "errno" -version = "0.3.14" +name = "time" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ - "libc", - "windows-sys 0.61.2", + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", ] [[package]] -name = "fastrand" -version = "2.4.1" +name = "time-core" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] -name = "foldhash" -version = "0.1.5" +name = "time-macros" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] [[package]] -name = "futures" -version = "0.3.32" +name = "tinystr" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "displaydoc", + "zerovec", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "tinyvec" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ - "futures-core", - "futures-sink", + "tinyvec_macros", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "futures-executor" -version = "0.3.32" +name = "tokio" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", ] [[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" +name = "tokio-macros" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] -name = "futures-sink" -version = "0.3.32" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] [[package]] -name = "futures-task" -version = "0.3.32" +name = "tokio-tungstenite" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] [[package]] -name = "futures-util" -version = "0.3.32" +name = "tokio-util" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ - "futures-channel", + "bytes", "futures-core", - "futures-io", - "futures-macro", "futures-sink", - "futures-task", - "memchr", "pin-project-lite", - "slab", + "tokio", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "tokio-vsock" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "8b319ef9394889dab2e1b4f0085b45ba11d0c79dc9d1a9d1afc057d009d0f1c7" dependencies = [ - "typenum", - "version_check", + "bytes", + "futures", + "libc", + "tokio", + "vsock", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", + "serde_core", ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "toml_edit" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "foldhash", + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] [[package]] -name = "heck" -version = "0.5.0" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] [[package]] -name = "hmac" -version = "0.12.1" +name = "tower-http" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "digest", + "async-compression", + "bitflags 2.13.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", ] [[package]] -name = "home" -version = "0.5.12" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "windows-sys 0.61.2", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] -name = "indexmap" -version = "2.14.0" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", + "once_cell", + "valuable", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] [[package]] -name = "libc" -version = "0.2.186" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "ttrpc" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "9f12efe237328bbe3b3b50503627f7cc9e74cfae34ffa8438561916623cd8f50" +dependencies = [ + "async-stream", + "async-trait", + "byteorder", + "crossbeam", + "futures", + "libc", + "log", + "nix 0.26.4", + "protobuf", + "protobuf-codegen", + "thiserror 1.0.69", + "tokio", + "tokio-vsock", + "windows-sys 0.48.0", +] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "tungstenite" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.7", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] [[package]] -name = "log" -version = "0.4.32" +name = "typenum" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] -name = "memchr" -version = "2.8.2" +name = "typespec" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "753a2fe021e407d4fc9ee6f4f0a33403cc306d5c54c4e4ebe1b8cbde0ca052b9" +dependencies = [ + "base64", + "bytes", + "futures", + "serde", + "serde_json", + "url", +] [[package]] -name = "memoffset" -version = "0.7.1" +name = "typespec_client_core" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "0373af0f9d4f580b3a1a9d9639cedaabe015ed262b35bfbe13941bfb14fe1ea6" dependencies = [ - "autocfg", + "async-trait", + "base64", + "bytes", + "dyn-clone", + "futures", + "pin-project", + "rand 0.10.2", + "reqwest 0.13.4", + "serde", + "serde_json", + "time", + "tokio", + "tracing", + "typespec", + "typespec_macros", + "url", + "uuid", ] [[package]] -name = "memoffset" -version = "0.9.1" +name = "typespec_macros" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "2c608f4427943f8adb211abc95c87672b1b98847152783507d54e3246e502f60" dependencies = [ - "autocfg", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", ] [[package]] -name = "mio" -version = "1.2.1" +name = "uapi" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "3bf073840d1b16485bfe28b4e752eccee38d9ac53f152adf869708e3136561e6" dependencies = [ + "cc", + "cfg-if", "libc", - "wasi", - "windows-sys 0.61.2", + "uapi-proc", ] [[package]] -name = "nix" -version = "0.26.4" +name = "uapi-proc" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "54de46f980cea7b2ae8d8f7f9f1c35cf7062c68343e99345ef73758f8e60975a" dependencies = [ - "bitflags 1.3.2", - "cfg-if", + "lazy_static", "libc", - "memoffset 0.7.1", - "pin-utils", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", ] [[package]] -name = "nix" -version = "0.31.3" +name = "uds_windows" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ - "bitflags 2.13.0", - "cfg-if", - "cfg_aliases", - "libc", "memoffset 0.9.1", + "tempfile", + "windows-sys 0.61.2", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "unicode-width" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "prettyplease" -version = "0.2.37" +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "proc-macro2", - "syn", + "crypto-common", + "subtle", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "protobuf" -version = "3.7.2" +name = "url" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ - "once_cell", - "protobuf-support", - "thiserror", + "form_urlencoded", + "idna", + "percent-encoding", + "serde", ] [[package]] -name = "protobuf-codegen" -version = "3.7.2" +name = "urlencoding" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" -dependencies = [ - "anyhow", - "once_cell", - "protobuf", - "protobuf-parse", - "regex", - "tempfile", - "thiserror", -] +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] -name = "protobuf-parse" -version = "3.7.2" +name = "utf-8" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" -dependencies = [ - "anyhow", - "indexmap", - "log", - "protobuf", - "protobuf-support", - "tempfile", - "thiserror", - "which", -] +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "protobuf-support" -version = "3.7.2" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" -dependencies = [ - "thiserror", -] +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "quote" -version = "1.0.45" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "r-efi" -version = "6.0.0" +name = "uuid" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] [[package]] -name = "regex" -version = "1.12.4" +name = "uzers" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "0b8275fb1afee25b4111d2dc8b5c505dbbc4afd0b990cb96deb2d88bff8be18d" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "libc", + "log", ] [[package]] -name = "regex-automata" -version = "0.4.14" +name = "valuable" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "regex-syntax" -version = "0.8.11" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "rustix" -version = "0.38.44" +name = "vsock" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "6ba782755fc073877e567c2253c0be48e4aa9a254c232d36d3985dfae0bd5205" dependencies = [ - "bitflags 2.13.0", - "errno", - "itoa", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "nix 0.31.3", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "same-file", + "winapi-util", ] [[package]] -name = "schemars" -version = "0.8.22" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "dyn-clone", - "schemars_derive", - "serde", - "serde_json", + "try-lock", ] [[package]] -name = "schemars_derive" -version = "0.8.22" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", + "wit-bindgen", ] [[package]] -name = "semver" -version = "1.0.28" +name = "wasm-bindgen" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] [[package]] -name = "serde" -version = "1.0.228" +name = "wasm-bindgen-futures" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ - "serde_core", - "serde_derive", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "wasm-bindgen-macro" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ - "serde_derive", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "wasm-bindgen-macro-support" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", + "wasm-bindgen-shared", ] [[package]] -name = "serde_derive_internals" -version = "0.29.1" +name = "wasm-bindgen-shared" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ - "proc-macro2", - "quote", - "syn", + "unicode-ident", ] [[package]] -name = "serde_json" -version = "1.0.150" +name = "wasm-streams" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "sha2" -version = "0.10.9" +name = "wayland-backend" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "cc", + "downcast-rs", + "rustix 1.1.4", + "smallvec", + "wayland-sys", ] [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "wayland-client" +version = "0.31.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" dependencies = [ - "errno", - "libc", + "bitflags 2.13.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", ] [[package]] -name = "slab" -version = "0.4.12" +name = "wayland-protocols" +version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] [[package]] -name = "socket2" -version = "0.6.4" +name = "wayland-protocols-misc" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "libc", - "windows-sys 0.61.2", + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", ] [[package]] -name = "subtle" -version = "2.6.1" +name = "wayland-protocols-wlr" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] [[package]] -name = "syn" -version = "2.0.117" +name = "wayland-scanner" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", + "quick-xml", "quote", - "unicode-ident", ] [[package]] -name = "tempfile" -version = "3.27.0" +name = "wayland-sys" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", + "pkg-config", ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "web-sys" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ - "thiserror-impl", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "tinyvec" -version = "1.12.0" +name = "web_atoms" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" dependencies = [ - "tinyvec_macros", + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.52.3" +name = "webpki-root-certs" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", + "rustls-pki-types", ] [[package]] -name = "tokio-macros" -version = "2.7.0" +name = "webpki-roots" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "proc-macro2", - "quote", - "syn", + "webpki-roots 1.0.9", ] [[package]] -name = "tokio-vsock" -version = "0.7.2" +name = "webpki-roots" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b319ef9394889dab2e1b4f0085b45ba11d0c79dc9d1a9d1afc057d009d0f1c7" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ - "bytes", - "futures", - "libc", - "tokio", - "vsock", + "rustls-pki-types", ] [[package]] -name = "ttrpc" -version = "0.9.0" +name = "which" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f12efe237328bbe3b3b50503627f7cc9e74cfae34ffa8438561916623cd8f50" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ - "async-stream", - "async-trait", - "byteorder", - "crossbeam", - "futures", - "libc", - "log", - "nix 0.26.4", - "protobuf", - "protobuf-codegen", - "thiserror", - "tokio", - "tokio-vsock", - "windows-sys 0.48.0", + "either", + "home", + "once_cell", + "rustix 0.38.44", ] [[package]] -name = "typenum" -version = "1.20.1" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "unicode-normalization" -version = "0.1.25" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "tinyvec", + "windows-sys 0.61.2", ] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "version_check" -version = "0.9.5" +name = "windows" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] [[package]] -name = "vsock" -version = "0.5.4" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba782755fc073877e567c2253c0be48e4aa9a254c232d36d3985dfae0bd5205" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "libc", - "nix 0.31.3", + "windows-core", ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "windows-core" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] [[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" +name = "windows-future" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "wit-bindgen 0.57.1", + "windows-core", + "windows-link 0.1.3", + "windows-threading", ] [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "wit-bindgen 0.51.0", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "leb128fmt", - "wasmparser", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "wasmparser" -version = "0.244.0" +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap", - "semver", + "windows-core", + "windows-link 0.1.3", ] [[package]] -name = "which" -version = "4.4.2" +name = "windows-result" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", + "windows-link 0.1.3", ] [[package]] -name = "windows-link" -version = "0.2.1" +name = "windows-strings" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] [[package]] name = "windows-sys" @@ -1125,6 +4957,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1140,7 +4981,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1174,6 +5015,24 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1265,12 +5124,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "winnow" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ - "wit-bindgen-rust-macro", + "memchr", ] [[package]] @@ -1280,86 +5139,245 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "wit-bindgen-core" -version = "0.51.0" +name = "wl-proxy" +version = "0.1.2" +source = "git+https://github.com/vicondoa/wl-proxy.git?rev=072945b59fef21a2a8166460454280d543f48772#072945b59fef21a2a8166460454280d543f48772" +dependencies = [ + "debug-fn", + "error_reporter", + "isnt", + "linearize", + "log", + "parking_lot", + "phf 0.13.1", + "run-on-drop", + "smallvec", + "thiserror 2.0.20", + "uapi", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "anyhow", - "heck", - "wit-parser", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "wit-bindgen-rust" -version = "0.51.0" +name = "yoke-derive" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", ] [[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "zbus" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" dependencies = [ - "anyhow", - "prettyplease", + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", + "syn 3.0.3", + "zbus_names", + "zvariant", + "zvariant_utils", ] [[package]] -name = "wit-component" -version = "0.244.0" +name = "zbus_names" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap", - "log", "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "winnow", + "zvariant", ] [[package]] -name = "wit-parser" -version = "0.244.0" +name = "zcheapstr" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.3", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.3", + "winnow", +] diff --git a/packages/d2b-priv-broker/BUILD.bazel b/packages/d2b-broker/BUILD.bazel similarity index 60% rename from packages/d2b-priv-broker/BUILD.bazel rename to packages/d2b-broker/BUILD.bazel index 9ffee07cc7..0d2d820034 100644 --- a/packages/d2b-priv-broker/BUILD.bazel +++ b/packages/d2b-broker/BUILD.bazel @@ -16,132 +16,126 @@ exports_files( visibility = ["//visibility:public"], ) -d2b_priv_broker_srcs = glob(["src/**/*.rs"], allow_empty = True) +d2b_broker_srcs = glob(["src/**/*.rs"], allow_empty = True) -d2b_priv_broker_compile_data = [ +d2b_broker_compile_data = [ "//tests/fixtures:deny-unknown/host-valid.json", # keep "//:tests/golden/manifest_v04/baseline-vms.json", # keep "Cargo.toml", "src/sys.rs", ] -d2b_priv_broker_shared_deps = [ +d2b_broker_shared_deps = [ "//packages/d2b-audit:d2b_audit", "//packages/d2b-contracts:d2b_contracts", "//packages/d2b-contracts-broker:d2b_contracts_broker", - "//packages/d2b-contracts-control:d2b_contracts_control", "//packages/d2b-realm-core:d2b_realm_core", "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", ] -d2b_priv_broker_test_support_deps = [ +d2b_broker_test_support_deps = [ "//packages/d2b-audit:d2b_audit_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", - "//packages/d2b-contracts-control:d2b_contracts_control", "//packages/d2b-realm-core:d2b_realm_core", "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", ] rust_library( - name = "d2b_priv_broker", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, visibility = ["//visibility:public"], - deps = d2b_priv_broker_shared_deps + [ + deps = d2b_broker_shared_deps + [ "//packages/d2b-core:d2b_core", "//packages/d2b-host:d2b_host", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_test_support", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, - crate_name = "d2b_priv_broker", + name = "d2b_broker_test_support", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_layer1_bootstrap", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_layer1_bootstrap", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["layer1-bootstrap"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_shared_deps + [ + deps = d2b_broker_shared_deps + [ "//packages/d2b-core:d2b_core", "//packages/d2b-host:d2b_host", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_layer1_bootstrap_test_support", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_layer1_bootstrap_test_support", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["layer1-bootstrap"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_fake_backends", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_fake_backends", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["fake-backends"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_fake_backends_test_support", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_fake_backends_test_support", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["fake-backends"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_binary( - name = "d2b-priv-broker", + name = "d2b-broker", srcs = ["src/main.rs"], compile_data = ["Cargo.toml"], visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker", + ":d2b_broker", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_binary( - name = "d2b-priv-broker-layer1-bootstrap", + name = "d2b-broker-layer1-bootstrap", srcs = ["src/main.rs"], compile_data = ["Cargo.toml"], crate_features = ["layer1-bootstrap"], visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_layer1_bootstrap", + ":d2b_broker_layer1_bootstrap", ] + all_crate_deps(normal = True, cargo_only = True), ) @@ -149,15 +143,14 @@ rust_test( name = "bridge_lifecycle", srcs = ["tests/bridge_lifecycle.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-core:d2b_core_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), @@ -172,14 +165,13 @@ rust_test( compile_data = ["Cargo.toml"], crate_features = ["layer1-bootstrap"], crate_root = "tests/broker_export_audit.rs", - data = [":d2b-priv-broker-layer1-bootstrap"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, + data = [":d2b-broker-layer1-bootstrap"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts:d2b_contracts_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -188,14 +180,13 @@ rust_test( name = "broker_protocol_compatibility", srcs = ["tests/broker_protocol_compatibility.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), @@ -210,14 +201,13 @@ rust_test( compile_data = ["Cargo.toml"], crate_features = ["layer1-bootstrap"], crate_root = "tests/broker_socket_acl.rs", - data = [":d2b-priv-broker-layer1-bootstrap"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, + data = [":d2b-broker-layer1-bootstrap"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -225,15 +215,14 @@ rust_test( name = "bundle_tampered_broker", srcs = ["tests/bundle_tampered_broker.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -244,15 +233,14 @@ rust_test( name = "kernel_surface", srcs = ["tests/kernel_surface.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -260,15 +248,14 @@ rust_test( name = "persistent_tap_lifecycle", srcs = ["tests/persistent_tap_lifecycle.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -280,15 +267,14 @@ rust_test( srcs = ["tests/pidfd_handoff_scm_rights.rs"], compile_data = ["Cargo.toml"], crate_features = ["fake-backends"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_fake_backends_test_support", + ":d2b_broker_fake_backends_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -296,15 +282,14 @@ rust_test( name = "pidfd_real_spawner", srcs = ["tests/pidfd_real_spawner.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -312,15 +297,14 @@ rust_test( name = "security_key_broker", srcs = ["tests/security_key_broker.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -332,20 +316,19 @@ rust_test( srcs = ["tests/socket_activation.rs"], compile_data = ["Cargo.toml"], data = [ - ":d2b-priv-broker", + ":d2b-broker", "//:tests/tools/scrub-shell-environment", ], env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "D2B_TEST_SCRUB_SHELL_ENVIRONMENT": "$(rootpath //:tests/tools/scrub-shell-environment)", }, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), @@ -355,15 +338,14 @@ rust_test( name = "w12_fd_passing_response", srcs = ["tests/w12_fd_passing_response.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -375,15 +357,14 @@ rust_test( "//:tests/golden/manifest_v04/baseline-vms.json", # keep "Cargo.toml", ], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -394,45 +375,43 @@ rust_test( name = "zone_store", srcs = ["tests/zone_store.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) rust_test( - name = "d2b_priv_broker_test", + name = "d2b_broker_test", compile_data = ["Cargo.toml"], - crate = ":d2b_priv_broker_test_support", - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + crate = ":d2b_broker_test_support", + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, rustc_env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "CARGO_MANIFEST_DIR": ".", }, tags = broker_test_tags(), deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) rust_test( - name = "d2b_priv_broker_layer1_bootstrap_test", + name = "d2b_broker_layer1_bootstrap_test", compile_data = ["Cargo.toml"], - crate = ":d2b_priv_broker_layer1_bootstrap_test_support", - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + crate = ":d2b_broker_layer1_bootstrap_test_support", + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, rustc_env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "CARGO_MANIFEST_DIR": ".", }, tags = broker_test_tags(), @@ -440,19 +419,71 @@ rust_test( ) rust_test( - name = "d2b_priv_broker_fake_backends_test", + name = "d2b_broker_fake_backends_test", compile_data = ["Cargo.toml"], - crate = ":d2b_priv_broker_fake_backends_test_support", - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + crate = ":d2b_broker_fake_backends_test_support", + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, rustc_env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "CARGO_MANIFEST_DIR": ".", }, tags = broker_test_tags(), deps = all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) +rust_test( + name = "host_profile", + srcs = ["tests/host_profile.rs"], + compile_data = ["Cargo.toml"], + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + tags = broker_test_tags(), + visibility = ["//visibility:public"], + deps = [ + "//packages/d2b-contracts-broker:d2b_contracts_broker", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "guest_profile", + srcs = [ + "tests/guest_profile.rs", + "tests/common/mod.rs", + ], + compile_data = ["Cargo.toml"], + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + tags = broker_test_tags(), + visibility = ["//visibility:public"], + deps = [ + "//packages/d2b-contracts:d2b_contracts_test_support", + "//packages/d2b-contracts-broker:d2b_contracts_broker", + "//packages/d2b-contracts-resource:d2b_contracts_resource", + ":d2b_broker_test_support", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "profile_separation", + srcs = [ + "tests/profile_separation.rs", + "tests/common/mod.rs", + ], + compile_data = ["Cargo.toml"], + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + tags = broker_test_tags(), + visibility = ["//visibility:public"], + deps = [ + "//packages/d2b-contracts-broker:d2b_contracts_broker", + ":d2b_broker_test_support", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + # keep filegroup( name = "cargo_workspace_sources", @@ -472,6 +503,6 @@ filegroup( ) rust_doc_test( - name = "d2b_priv_broker_doc_test", - crate = "//packages/d2b-priv-broker:d2b_priv_broker", + name = "d2b_broker_doc_test", + crate = "//packages/d2b-broker:d2b_broker", ) diff --git a/packages/d2b-priv-broker/Cargo.toml b/packages/d2b-broker/Cargo.toml similarity index 94% rename from packages/d2b-priv-broker/Cargo.toml rename to packages/d2b-broker/Cargo.toml index 7454f77b1c..3846f6cfa8 100644 --- a/packages/d2b-priv-broker/Cargo.toml +++ b/packages/d2b-broker/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "d2b-priv-broker" +name = "d2b-broker" version = "0.0.0-bootstrap" edition = "2024" publish = false @@ -35,11 +35,9 @@ layer1-bootstrap = [] fake-backends = ["d2b-host/fake-backends"] [dependencies] -d2b-contracts-zone-session = { path = "../d2b-contracts-zone-session", version = "0.0.0-bootstrap" } d2b-audit = { path = "../d2b-audit", version = "0.0.0-bootstrap" } d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } d2b-contracts-broker = { path = "../d2b-contracts-broker", version = "0.0.0-bootstrap" } -d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } d2b-core = { path = "../d2b-core", version = "0.0.0-bootstrap" } d2b-host = { path = "../d2b-host", version = "0.0.0-bootstrap" } diff --git a/packages/d2b-priv-broker/src/audit.rs b/packages/d2b-broker/src/audit.rs similarity index 99% rename from packages/d2b-priv-broker/src/audit.rs rename to packages/d2b-broker/src/audit.rs index 0bd57364b9..227fd1e4cd 100644 --- a/packages/d2b-priv-broker/src/audit.rs +++ b/packages/d2b-broker/src/audit.rs @@ -528,7 +528,7 @@ impl AuditLog { /// Legacy short-record writer for errored outcomes that need /// admin-visible diagnostics. The full detail is also surfaced in - /// the broker journal (`journalctl -u d2b-priv-broker`) for + /// the broker journal (`journalctl -u d2b-broker`) for /// live-handler failures. pub fn write_error_entry( &self, diff --git a/packages/d2b-priv-broker/src/bootstrap.rs b/packages/d2b-broker/src/bootstrap.rs similarity index 100% rename from packages/d2b-priv-broker/src/bootstrap.rs rename to packages/d2b-broker/src/bootstrap.rs diff --git a/packages/d2b-priv-broker/src/fd_passing.rs b/packages/d2b-broker/src/fd_passing.rs similarity index 100% rename from packages/d2b-priv-broker/src/fd_passing.rs rename to packages/d2b-broker/src/fd_passing.rs diff --git a/packages/d2b-priv-broker/src/lib.rs b/packages/d2b-broker/src/lib.rs similarity index 95% rename from packages/d2b-priv-broker/src/lib.rs rename to packages/d2b-broker/src/lib.rs index bc5b7d12b0..761f7ca69c 100644 --- a/packages/d2b-priv-broker/src/lib.rs +++ b/packages/d2b-broker/src/lib.rs @@ -47,12 +47,14 @@ pub mod protocol; pub mod runtime; pub mod sys; +pub use d2b_contracts_broker::broker_wire::BrokerProfile; + #[cfg(test)] pub(crate) fn test_scratch_root() -> std::path::PathBuf { let root = std::env::var_os("TEST_TMPDIR") .or_else(|| std::env::var_os("CARGO_TARGET_TMPDIR")) .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::env::temp_dir().join("d2b-priv-broker-tests")); + .unwrap_or_else(|| std::env::temp_dir().join("d2b-broker-tests")); std::fs::create_dir_all(&root).expect("create test scratch root"); root } diff --git a/packages/d2b-priv-broker/src/live_handlers.rs b/packages/d2b-broker/src/live_handlers.rs similarity index 89% rename from packages/d2b-priv-broker/src/live_handlers.rs rename to packages/d2b-broker/src/live_handlers.rs index 38aeb17e84..39e39cc854 100644 --- a/packages/d2b-priv-broker/src/live_handlers.rs +++ b/packages/d2b-broker/src/live_handlers.rs @@ -36,6 +36,9 @@ use crate::ops::spawn_runner::{ use crate::ops::store_sync::generation_id_for_intent; use crate::ops::store_view_posture::plant_live_marker_with_matrix_posture; use d2b_contracts_broker::broker_wire::{ActivationMode, ActivationPhase}; +use d2b_contracts_resource::v3::{ + ActivationRunnerInput, MAX_ACTIVATION_RUNNER_INPUT_BYTES, +}; use d2b_core::bundle_resolver::{HostRuntime, ResolvedActivationIntent, ResolvedStoreViewIntent}; use d2b_core::minijail_profile::CgroupPlacement; use d2b_host::hardlink_farm; @@ -1988,13 +1991,16 @@ pub(crate) fn policy_ref_device_classes( // virtiofsd accesses /dev/fuse via read/write; FUSE_NO_IOCTL // sentinel → permissive BPF (FUSE mount handshake needs ioctls). "w1-virtiofsd" => Some(&[DeviceClass::Fuse]), - // host-reconcile, store-virtiofs-preflight, guest-control-health: + // host-reconcile, store-virtiofs-preflight, component-session-health: // no device binds → permissive BPF. host-reconcile and // store-virtiofs-preflight run the nix toolchain (many ioctls for - // terminal/file operations); guest-control-health is the daemon-side - // authenticated Health probe, which speaks ttRPC over the guest-control + // terminal/file operations); component-session-health is the daemon-side + // authenticated Health probe, which speaks ttRPC over the component-session // vsock and uses connect(2)/socket ioctls. - "w1-host-reconcile" | "w1-store-virtiofs-preflight" | "w1-guest-control-health" => { + "w1-host-reconcile" + | "w1-store-virtiofs-preflight" + | "w1-component-session-health" + | "w1-activation-nixos-runner" => { Some(&[]) } // swtpm is a software TPM emulator; no hardware device ioctls, @@ -2278,7 +2284,7 @@ fn setfacl_fd_safe(path: &Path, acl_spec: &str, kind: AclPathKind) -> Result<(), } /// The fd-safe setfacl stage that failed. A closed-set classification so -/// guest-control callers can build path-free, acl-spec-free error +/// component-session callers can build path-free, acl-spec-free error /// details (the raw path / acl spec never escapes into logs or audit). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SetfaclStage { @@ -2293,9 +2299,9 @@ enum SetfaclStage { /// `legacy_detail` carries the historical path-bearing message for the /// observability/device/session callers that already embed paths in /// their error strings. The structured `stage` / `errno_kind` / -/// `raw_os_error` fields let the guest-control path build a path-free, +/// `raw_os_error` fields let the component-session path build a path-free, /// acl-spec-free detail that satisfies the hash-only observability -/// contract. The guest-control formatter MUST NOT read `legacy_detail`. +/// contract. The component-session formatter MUST NOT read `legacy_detail`. #[derive(Debug, PartialEq, Eq)] struct SetfaclFailure { stage: SetfaclStage, @@ -2314,37 +2320,19 @@ impl SetfaclFailure { } } - /// Path-free, acl-spec-free failure detail for the guest-control + /// Path-free, acl-spec-free failure detail for the component-session /// observability contract. Carries only the closed-set operation /// label, target class, daemon principal, failed stage, and the /// numeric errno / `io::ErrorKind`. Never the raw socket / state-dir /// path or the acl-spec string. - fn guest_control_detail(&self, op_label: &str, target_class: &str) -> String { + fn component_session_detail(&self, op_label: &str, target_class: &str) -> String { let errno = self .raw_os_error .map(|code| code.to_string()) .unwrap_or_else(|| "none".to_owned()); format!( - "guest-control vsock daemon ACL {op_label} on {target_class} failed: \ - principal={GUEST_CONTROL_DAEMON_PRINCIPAL} stage={} kind={:?} errno={errno}", - self.stage_label(), - self.errno_kind, - ) - } - - /// Path-free detail for the guest-control fs-share (`d2b-gctl`) consumer - /// ACL path. The consumer is the per-VM cloud-hypervisor runner (not the - /// daemon), so this reports the CH-runner principal class rather than - /// `d2bd`. Carries only closed-set op/target/stage/errno labels - - /// never the raw socket/state-dir path, the acl-spec, or a uid-by-value. - fn guest_control_fs_detail(&self, op_label: &str, target_class: &str) -> String { - let errno = self - .raw_os_error - .map(|code| code.to_string()) - .unwrap_or_else(|| "none".to_owned()); - format!( - "guest-control fs-share consumer ACL {op_label} on {target_class} failed: \ - principal={GUEST_CONTROL_FS_CONSUMER_PRINCIPAL} stage={} kind={:?} errno={errno}", + "component-session vsock daemon ACL {op_label} on {target_class} failed: \ + principal={COMPONENT_SESSION_DAEMON_PRINCIPAL} stage={} kind={:?} errno={errno}", self.stage_label(), self.errno_kind, ) @@ -2438,7 +2426,7 @@ fn setfacl_fd_safe_op_classed( /// path was absent). The returned identity is consumed by path-free /// audit hashing so audit records never carry raw socket/state-dir /// paths. The error string is the historical path-bearing form for -/// the observability/device/session callers; guest-control callers use +/// the observability/device/session callers; component-session callers use /// [`setfacl_fd_safe_op_classed`] for a path-free detail instead. fn setfacl_fd_safe_op( path: &Path, @@ -2956,8 +2944,7 @@ fn refresh_spawn_runner_acls(plan: &SpawnRunnerPlan) -> Result<(), LiveHandlerEr } } refresh_obs_vsock_acl(plan)?; - refresh_guest_control_vsock_acl(plan)?; - refresh_guest_control_fs_acl(plan)?; + refresh_component_session_vsock_acl(plan)?; Ok(()) } @@ -2995,40 +2982,17 @@ fn grant_daemon_api_socket_acl(api_socket: PathBuf) { }); } -/// The system principal the framework grants daemon-side guest-control +/// The system principal the framework grants daemon-side component-session /// vsock access to. The `d2bd` daemon owns the per-VM lifecycle DAG -/// and is the only host process that connects to the guest-control +/// and is the only host process that connects to the component-session /// vsock socket for the readiness probe / config-sync over the bridge. -const GUEST_CONTROL_DAEMON_PRINCIPAL: &str = "d2bd"; - -/// The runner principal-class the framework grants guest-control fs-share -/// (`d2b-gctl`) connect access to: the per-VM cloud-hypervisor runner. The -/// `d2b-gctl` token virtiofs share is the only CROSS-PRINCIPAL fs share - -/// it is served by the narrower `gctlfs` principal (ADR 0021), so its 0700 -/// socket is owned by `gctlfs`, not the CH runner. CH connects to that -/// vhost-user fs backend socket during device-init (the `--fs -/// socket=...,tag=d2b-gctl` element emitted by `processes-json.nix`), but -/// the inherited `default:u:$ch_uid` grant is masked out by the 0700 -/// socket's `mask::---`. This label names the consumer for the hash-only -/// audit without leaking a uid-by-value. -const GUEST_CONTROL_FS_CONSUMER_PRINCIPAL: &str = "cloud-hypervisor-runner"; - -/// The setfacl `-m` spec that lifts the `d2b-gctl` socket's masked-out -/// CH-runner named entry: grant the runner uid `rw` AND pin the ACL mask -/// to `rw`. The explicit `m::rw` is load-bearing - without it the 0700 -/// socket's `mask::---` keeps the named entry's effective perms at `---` -/// and CH's connect still EACCESes. The mask grants no execute and does -/// not touch owner/owning-group/other, so it raises effective perms for -/// the CH-runner named entry only. -fn guest_control_fs_socket_acl_spec(uid: u32) -> String { - format!("u:{uid}:rw,m::rw") -} +const COMPONENT_SESSION_DAEMON_PRINCIPAL: &str = "d2bd"; /// Extract the cloud-hypervisor `--vsock socket=` argument for a /// CH runner plan. Gated on the CH runner's seccomp policy ref so the /// daemon-vsock ACL is only ever attached to a real cloud-hypervisor /// runner's vsock socket, never to any other role's argv. -fn cloud_hypervisor_vsock_socket_arg(plan: &SpawnRunnerPlan) -> Option { +fn cloud_hypervisor_component_session_socket_arg(plan: &SpawnRunnerPlan) -> Option { if plan.seccomp_policy_ref.as_deref() != Some("w1-cloud-hypervisor-runner") { return None; } @@ -3042,17 +3006,17 @@ fn cloud_hypervisor_vsock_socket_arg(plan: &SpawnRunnerPlan) -> Option }) } -/// Path-free digest of a guest-control vsock ACL mutation, for audit. +/// Path-free digest of a component-session vsock ACL mutation, for audit. /// /// Returns `sha256:` over the operation, the target class, and the /// target's resolved `(dev, ino)` - never the raw socket / state-dir -/// path. The guest-control observability contract forbids raw vsock / +/// path. The component-session observability contract forbids raw vsock / /// socket / state-dir paths in spans, logs, metrics, and audit. -fn guest_control_acl_diff_hash(op: &str, target_class: &str, dev: u64, ino: u64) -> String { +fn component_session_acl_diff_hash(op: &str, target_class: &str, dev: u64, ino: u64) -> String { use sha2::Digest as _; use std::fmt::Write as _; let mut hasher = sha2::Sha256::new(); - hasher.update(b"guest-control-vsock-acl\0"); + hasher.update(b"component-session-vsock-acl\0"); hasher.update(op.as_bytes()); hasher.update([0]); hasher.update(target_class.as_bytes()); @@ -3067,26 +3031,26 @@ fn guest_control_acl_diff_hash(op: &str, target_class: &str, dev: u64, ino: u64) out } -/// Emit a hash-only audit event for a guest-control vsock ACL mutation. +/// Emit a hash-only audit event for a component-session vsock ACL mutation. /// Closed-enum labels only; no raw paths, uids-by-value, or content. -fn audit_guest_control_vsock_acl(op: &str, target_class: &str, dev: u64, ino: u64) { +fn audit_component_session_vsock_acl(op: &str, target_class: &str, dev: u64, ino: u64) { tracing::info!( kind = "critical", - subsystem = "guest-control-health", + subsystem = "component-session-health", op = op, - daemon_principal = GUEST_CONTROL_DAEMON_PRINCIPAL, + daemon_principal = COMPONENT_SESSION_DAEMON_PRINCIPAL, target_class = target_class, - acl_diff_hash = %guest_control_acl_diff_hash(op, target_class, dev, ino), + acl_diff_hash = %component_session_acl_diff_hash(op, target_class, dev, ino), result = "ok", - "guest-control vsock daemon ACL mutation", + "component-session vsock daemon ACL mutation", ); } /// Path-free wrapper over [`setfacl_fd_safe_op_classed`] for the -/// guest-control ACL path: on failure, builds a detail string carrying +/// component-session ACL path: on failure, builds a detail string carrying /// only the closed-set op/target-class/stage/errno classification - /// never the raw socket/state-dir path or the acl-spec string. -fn setfacl_guest_control( +fn setfacl_component_session( path: &Path, op: &str, acl_spec: &str, @@ -3095,41 +3059,7 @@ fn setfacl_guest_control( target_class: &str, ) -> Result, String> { setfacl_fd_safe_op_classed(path, op, acl_spec, kind) - .map_err(|failure| failure.guest_control_detail(op_label, target_class)) -} - -/// Path-free wrapper over [`setfacl_fd_safe_op_classed`] for the -/// guest-control fs-share (`d2b-gctl`) CONSUMER ACL path: identical to -/// [`setfacl_guest_control`] but reports the cloud-hypervisor-runner -/// consumer principal class on failure instead of the daemon principal. -fn setfacl_guest_control_fs( - path: &Path, - op: &str, - acl_spec: &str, - kind: AclPathKind, - op_label: &str, - target_class: &str, -) -> Result, String> { - setfacl_fd_safe_op_classed(path, op, acl_spec, kind) - .map_err(|failure| failure.guest_control_fs_detail(op_label, target_class)) -} - -/// Emit a hash-only audit event for a guest-control fs-share consumer ACL -/// mutation (the cloud-hypervisor runner's connect grant on the `d2b-gctl` -/// virtiofs socket / its parent dir). Closed-enum labels only; no raw -/// paths, uids-by-value, or content - same contract as -/// [`audit_guest_control_vsock_acl`]. -fn audit_guest_control_fs_acl(op: &str, target_class: &str, dev: u64, ino: u64) { - tracing::info!( - kind = "critical", - subsystem = "guest-control-health", - op = op, - consumer_principal = GUEST_CONTROL_FS_CONSUMER_PRINCIPAL, - target_class = target_class, - acl_diff_hash = %guest_control_acl_diff_hash(op, target_class, dev, ino), - result = "ok", - "guest-control fs-share consumer ACL mutation", - ); + .map_err(|failure| failure.component_session_detail(op_label, target_class)) } /// Whether the daemon needs an explicit `--x` grant to traverse `path`. @@ -3157,14 +3087,14 @@ fn current_path_dev_ino(path: &Path) -> Result, SetfaclFailur /// Grant the daemon `u:d2bd:--x` on every non-world-traversable /// directory from the filesystem root down to `leaf` (inclusive), so the -/// daemon can `connect()` to the per-VM guest-control socket through the +/// daemon can `connect()` to the per-VM component-session socket through the /// full ancestor chain - not just the immediate parent. World- /// traversable directories already grant search to everyone and are /// skipped. The immediate per-VM leaf is audited as `state-dir`; higher /// non-world-x ancestors as `ancestor`. These grants are additive and /// idempotent; they are never revoked because sibling VMs and the /// per-VM api-socket also depend on them. -fn grant_guest_control_traversal_acls(leaf: &Path) -> Result<(), String> { +fn grant_component_session_traversal_acls(leaf: &Path) -> Result<(), String> { let mut chain: Vec<&Path> = leaf .ancestors() .filter(|component| !component.as_os_str().is_empty()) @@ -3178,24 +3108,24 @@ fn grant_guest_control_traversal_acls(leaf: &Path) -> Result<(), String> { "ancestor" }; let needs = dir_needs_daemon_traverse(dir) - .map_err(|failure| failure.guest_control_detail("grant", target_class))?; + .map_err(|failure| failure.component_session_detail("grant", target_class))?; if needs == Some(true) - && let Some((dev, ino)) = setfacl_guest_control( + && let Some((dev, ino)) = setfacl_component_session( dir, "-m", - &format!("u:{GUEST_CONTROL_DAEMON_PRINCIPAL}:--x"), + &format!("u:{COMPONENT_SESSION_DAEMON_PRINCIPAL}:--x"), AclPathKind::Directory, "grant", target_class, )? { - audit_guest_control_vsock_acl("grant", target_class, dev, ino); + audit_component_session_vsock_acl("grant", target_class, dev, ino); } } Ok(()) } -/// Grant `u:d2bd:rw` on the guest-control vsock socket inode, then +/// Grant `u:d2bd:rw` on the component-session vsock socket inode, then /// re-stat the path to confirm it still resolves to the same `(dev, /// ino)` the fd-based setfacl mutated (inode pinning). If the /// socket was replaced (or vanished) between the setfacl and the @@ -3203,11 +3133,11 @@ fn grant_guest_control_traversal_acls(leaf: &Path) -> Result<(), String> { /// and report not-ready (`Ok(false)`) so the caller retries against the /// current, live inode. Returns `Ok(false)` while the socket has not yet /// been created by cloud-hypervisor. -fn grant_guest_control_socket_acl_once(socket: &Path) -> Result { - let Some((dev, ino)) = setfacl_guest_control( +fn grant_component_session_socket_acl_once(socket: &Path) -> Result { + let Some((dev, ino)) = setfacl_component_session( socket, "-m", - &format!("u:{GUEST_CONTROL_DAEMON_PRINCIPAL}:rw"), + &format!("u:{COMPONENT_SESSION_DAEMON_PRINCIPAL}:rw"), AclPathKind::Socket, "grant", "vsock-socket", @@ -3216,17 +3146,17 @@ fn grant_guest_control_socket_acl_once(socket: &Path) -> Result { return Ok(false); }; match current_path_dev_ino(socket) - .map_err(|failure| failure.guest_control_detail("grant", "vsock-socket"))? + .map_err(|failure| failure.component_session_detail("grant", "vsock-socket"))? { Some(current) if current == (dev, ino) => { - audit_guest_control_vsock_acl("grant", "vsock-socket", dev, ino); + audit_component_session_vsock_acl("grant", "vsock-socket", dev, ino); Ok(true) } _ => Ok(false), } } -/// Revoke the daemon-principal guest-control vsock ACL from the socket +/// Revoke the daemon-principal component-session vsock ACL from the socket /// inode. Best-effort, idempotent, and path-free: a missing socket is a /// no-op. Scoped to the per-VM socket inode only - the shared/ancestor /// traversal grants are intentionally retained (the daemon also needs @@ -3235,16 +3165,16 @@ fn grant_guest_control_socket_acl_once(socket: &Path) -> Result { /// carrying the socket path (`SignalRunner` has only vm_id/role_id/ /// signal), so revoke runs as a revoke-then-grant at the next CH /// (re-)spawn so a replaced/disabled socket cannot retain a stale grant. -fn revoke_guest_control_vsock_acl(socket: &Path) -> Result<(), String> { - if let Some((dev, ino)) = setfacl_guest_control( +fn revoke_component_session_vsock_acl(socket: &Path) -> Result<(), String> { + if let Some((dev, ino)) = setfacl_component_session( socket, "-x", - &format!("u:{GUEST_CONTROL_DAEMON_PRINCIPAL}"), + &format!("u:{COMPONENT_SESSION_DAEMON_PRINCIPAL}"), AclPathKind::Socket, "revoke", "vsock-socket", )? { - audit_guest_control_vsock_acl("revoke", "vsock-socket", dev, ino); + audit_component_session_vsock_acl("revoke", "vsock-socket", dev, ino); } Ok(()) } @@ -3254,24 +3184,24 @@ fn revoke_guest_control_vsock_acl(socket: &Path) -> Result<(), String> { /// ~30s, matching the obs-vsock precedent). The traversal ACLs are /// granted synchronously before this thread starts, so the retry only /// re-attempts the socket grant. Logs are path-free. -fn spawn_guest_control_vsock_acl_retry(socket: PathBuf) { +fn spawn_component_session_vsock_acl_retry(socket: PathBuf) { std::thread::spawn(move || { for _ in 0..120 { - match grant_guest_control_socket_acl_once(&socket) { + match grant_component_session_socket_acl_once(&socket) { Ok(true) => return, Ok(false) => {} Err(_) => { tracing::debug!( - subsystem = "guest-control-health", - "guest-control vsock daemon ACL refresh not ready yet", + subsystem = "component-session-health", + "component-session vsock daemon ACL refresh not ready yet", ); } } std::thread::sleep(std::time::Duration::from_millis(250)); } tracing::warn!( - subsystem = "guest-control-health", - "guest-control vsock daemon ACL refresh timed out", + subsystem = "component-session-health", + "component-session vsock daemon ACL refresh timed out", ); }); } @@ -3286,191 +3216,38 @@ fn spawn_guest_control_vsock_acl_retry(socket: PathBuf) { /// daemon never loses search on the per-VM state dir (the api-socket /// depends on it too); if the socket is not yet present, a bounded retry /// thread completes the socket grant. -fn refresh_guest_control_vsock_acl(plan: &SpawnRunnerPlan) -> Result<(), LiveHandlerError> { - let Some(socket) = cloud_hypervisor_vsock_socket_arg(plan) else { +fn refresh_component_session_vsock_acl(plan: &SpawnRunnerPlan) -> Result<(), LiveHandlerError> { + let Some(socket) = cloud_hypervisor_component_session_socket_arg(plan) else { return Ok(()); }; let Some(parent) = socket.parent().map(Path::to_path_buf) else { return Err(LiveHandlerError::SpawnFailed { - detail: "guest-control vsock path has no parent".to_owned(), + detail: "component-session vsock path has no parent".to_owned(), }); }; - if let Err(detail) = revoke_guest_control_vsock_acl(&socket) { + if let Err(detail) = revoke_component_session_vsock_acl(&socket) { tracing::debug!( - subsystem = "guest-control-health", + subsystem = "component-session-health", detail = %detail, - "pre-grant guest-control daemon ACL revoke (best-effort)", + "pre-grant component-session daemon ACL revoke (best-effort)", ); } - grant_guest_control_traversal_acls(&parent).map_err(|detail| { + grant_component_session_traversal_acls(&parent).map_err(|detail| { LiveHandlerError::SpawnFailed { - detail: format!("refresh guest-control traversal ACLs: {detail}"), + detail: format!("refresh component-session traversal ACLs: {detail}"), } })?; - match grant_guest_control_socket_acl_once(&socket) { + match grant_component_session_socket_acl_once(&socket) { Ok(true) => Ok(()), Ok(false) => { - spawn_guest_control_vsock_acl_retry(socket); + spawn_component_session_vsock_acl_retry(socket); Ok(()) } Err(detail) => Err(LiveHandlerError::SpawnFailed { - detail: format!("refresh guest-control vsock daemon ACL: {detail}"), - }), - } -} - -/// Extract the cloud-hypervisor `--fs socket=,tag=d2b-gctl` argument -/// for a CH runner plan, or `None` for any non-CH runner / a CH plan -/// without the guest-control token share. -/// -/// Cloud Hypervisor's `--fs` takes a variadic list of value elements -/// (`socket=...,tag=...`) - one per share - so this scans EVERY argv -/// element (never just the first after `--fs`) for the element whose -/// comma-separated fields include exactly `tag=d2b-gctl`, and returns its -/// absolute `socket=` value. Gated on the CH runner's seccomp policy ref -/// so the grant is only ever attached to a real cloud-hypervisor runner. -fn cloud_hypervisor_guest_control_fs_socket_arg(plan: &SpawnRunnerPlan) -> Option { - if plan.seccomp_policy_ref.as_deref() != Some("w1-cloud-hypervisor-runner") { - return None; - } - plan.argv.iter().find_map(|arg| { - let arg = arg.trim_matches('"'); - let fields: Vec<&str> = arg.split(',').collect(); - if !fields.contains(&"tag=d2b-gctl") { - return None; - } - fields - .iter() - .find_map(|field| field.strip_prefix("socket=")) - .map(PathBuf::from) - .filter(|path| path.is_absolute()) - }) -} - -/// Grant the CH runner uid execute (search) on the `d2b-gctl` socket's -/// parent (the per-VM `guest-control` dir) so CH can traverse to the -/// socket. Idempotent with the host-activation `u:$ch_uid:--x` grant; -/// re-asserting it here makes the spawn path self-healing if the dir was -/// recreated. Path-free + hash-only audited. -fn grant_guest_control_fs_traversal_acl(parent: &Path, uid: u32) -> Result<(), String> { - if let Some((dev, ino)) = setfacl_guest_control_fs( - parent, - "-m", - &format!("u:{uid}:--x"), - AclPathKind::Directory, - "grant", - "gctlfs-dir", - )? { - audit_guest_control_fs_acl("grant", "gctlfs-dir", dev, ino); - } - Ok(()) -} - -/// Grant `u::rw` on the `d2b-gctl` virtiofs socket inode, with an -/// explicit `m::rw` mask so the 0700 socket's `mask::---` is deterministically -/// lifted to cover the CH-runner named entry (without enabling execute in the -/// mask). Then re-stat to confirm the same `(dev, ino)` (inode pinning): if -/// the socket was replaced/vanished between the setfacl and the re-stat, the -/// grant landed on a stale inode - do not audit success and report not-ready -/// (`Ok(false)`) so the caller retries the live inode. `Ok(false)` while the -/// socket has not yet been created by the gctlfs virtiofsd. -fn grant_guest_control_fs_socket_acl_once(socket: &Path, uid: u32) -> Result { - let Some((dev, ino)) = setfacl_guest_control_fs( - socket, - "-m", - &guest_control_fs_socket_acl_spec(uid), - AclPathKind::Socket, - "grant", - "gctlfs-socket", - )? - else { - return Ok(false); - }; - match current_path_dev_ino(socket) - .map_err(|failure| failure.guest_control_fs_detail("grant", "gctlfs-socket"))? - { - Some(current) if current == (dev, ino) => { - audit_guest_control_fs_acl("grant", "gctlfs-socket", dev, ino); - Ok(true) - } - _ => Ok(false), - } -} - -/// Retry the CH-runner `d2b-gctl` socket ACL grant in a background thread -/// until the gctlfs virtiofsd has created the socket. Bounded to ~60s -/// (240 × 250ms) so it covers cloud-hypervisor's own ~1-minute vhost-user -/// backend connect-retry window. The traversal grant is applied -/// synchronously before this thread starts; this only re-attempts the -/// socket grant. Logs are path-free. -fn spawn_guest_control_fs_acl_retry(socket: PathBuf, uid: u32) { - std::thread::spawn(move || { - for _ in 0..240 { - match grant_guest_control_fs_socket_acl_once(&socket, uid) { - Ok(true) => return, - Ok(false) => {} - Err(_) => { - tracing::debug!( - subsystem = "guest-control-health", - consumer_principal = GUEST_CONTROL_FS_CONSUMER_PRINCIPAL, - "guest-control fs-share consumer ACL refresh not ready yet", - ); - } - } - std::thread::sleep(std::time::Duration::from_millis(250)); - } - tracing::warn!( - subsystem = "guest-control-health", - consumer_principal = GUEST_CONTROL_FS_CONSUMER_PRINCIPAL, - "guest-control fs-share consumer ACL refresh timed out", - ); - }); -} - -/// Grant the cloud-hypervisor runner connect access to the `d2b-gctl` -/// virtiofs socket. No-op for any non-CH runner or a CH plan without the -/// guest-control token share. -/// -/// The `d2b-gctl` share is the only CROSS-PRINCIPAL fs share (served by the -/// narrower `gctlfs` principal, so CH does not own its socket as it does -/// the runner-owned shares). The gctlfs virtiofsd runs in the broker's -/// user namespace (ADR 0021) where `--socket-group` does not take effect -/// on the host-visible socket, leaving it `0700 gctlfs:gctlfs` with -/// `mask::---` - which masks out the `default:u:$ch_uid` grant the -/// host-activation default ACL inherits onto the socket. Grant the CH -/// runner uid search on the parent and `rw` on the socket (lifting the -/// mask) so CH's vhost-user backend connect succeeds. The socket is a DAG -/// predecessor of cloud-hypervisor, so the synchronous grant normally -/// lands before CH spawns; a bounded retry covers the rare absent-socket -/// race. -fn refresh_guest_control_fs_acl(plan: &SpawnRunnerPlan) -> Result<(), LiveHandlerError> { - let Some(socket) = cloud_hypervisor_guest_control_fs_socket_arg(plan) else { - return Ok(()); - }; - let Some(parent) = socket.parent().map(Path::to_path_buf) else { - return Err(LiveHandlerError::SpawnFailed { - detail: "guest-control fs-share socket path has no parent".to_owned(), - }); - }; - let uid = plan.uid; - - grant_guest_control_fs_traversal_acl(&parent, uid).map_err(|detail| { - LiveHandlerError::SpawnFailed { - detail: format!("refresh guest-control fs-share traversal ACL: {detail}"), - } - })?; - - match grant_guest_control_fs_socket_acl_once(&socket, uid) { - Ok(true) => Ok(()), - Ok(false) => { - spawn_guest_control_fs_acl_retry(socket, uid); - Ok(()) - } - Err(detail) => Err(LiveHandlerError::SpawnFailed { - detail: format!("refresh guest-control fs-share consumer ACL: {detail}"), + detail: format!("refresh component-session vsock daemon ACL: {detail}"), }), } } @@ -3491,6 +3268,7 @@ fn refresh_guest_control_fs_acl(plan: &SpawnRunnerPlan) -> Result<(), LiveHandle pub fn live_spawn_runner( plan_input: &SpawnRunnerPlanInput, mut pre_opened_device_fds: Vec, + activation_input: Option<&ActivationRunnerInput>, ) -> Result { let plan = preflight(plan_input).map_err(LiveHandlerError::SpawnPreflight)?; @@ -3568,6 +3346,20 @@ pub fn live_spawn_runner( let memlock_limit_bytes = memlock_guest_bytes.map(|guest_bytes| { guest_bytes.saturating_add(qemu_media_memlock_headroom_bytes(guest_bytes)) }); + let activation_stdin = activation_input + .map(serde_json::to_vec) + .transpose() + .map_err(|error| LiveHandlerError::SpawnFailed { + detail: format!("activation input serialization failed: {error}"), + })?; + if activation_stdin + .as_ref() + .is_some_and(|bytes| bytes.len() > MAX_ACTIVATION_RUNNER_INPUT_BYTES) + { + return Err(LiveHandlerError::SpawnFailed { + detail: "activation input exceeds bounded stdin envelope".to_owned(), + }); + } if let Some(guest_bytes) = memlock_guest_bytes { qemu_media_preflight_memlock_budget(qemu_media_memlock_preflight_required_bytes( guest_bytes, @@ -3606,6 +3398,7 @@ pub fn live_spawn_runner( // execve. pre_opened_device_fds, memlock_limit_bytes, + activation_stdin, }; let outcome = crate::sys::pidfd_sys::clone3_spawn_runner( @@ -3820,7 +3613,7 @@ mod tests { file_path: root.join("00-d2b-unmanaged.conf"), contents: concat!( "# d2b-managed begin\n", - "# Generated by d2b-priv-broker; do not edit by hand.\n", + "# Generated by d2b-broker; do not edit by hand.\n", "[keyfile]\n", "unmanaged-devices=interface-name:d2b-*\n", "# marker-id=nm-unmanaged:host\n", @@ -5042,7 +4835,7 @@ mod tests { user_namespace: None, umask: None, }; - let err = live_spawn_runner(&plan, Vec::new()).unwrap_err(); + let err = live_spawn_runner(&plan, Vec::new(), None).unwrap_err(); assert!(matches!(err, LiveHandlerError::SpawnPreflight(_))); } @@ -5250,7 +5043,7 @@ mod tests { ); assert_eq!( - cloud_hypervisor_vsock_socket_arg(&plan), + cloud_hypervisor_component_session_socket_arg(&plan), Some(PathBuf::from("/var/lib/d2b/vms/corp-vm/vsock.sock")) ); } @@ -5268,7 +5061,7 @@ mod tests { "w1-vsock-relay", ); - assert_eq!(cloud_hypervisor_vsock_socket_arg(&plan), None); + assert_eq!(cloud_hypervisor_component_session_socket_arg(&plan), None); } #[test] @@ -5282,183 +5075,14 @@ mod tests { "w1-cloud-hypervisor-runner", ); - assert_eq!(cloud_hypervisor_vsock_socket_arg(&plan), None); - } - - #[test] - fn parses_guest_control_fs_socket_for_ch_runner_heavy_vm() { - // CH `--fs` is variadic: many `socket=...,tag=...` value elements, - // with `d2b-gctl` LAST after ro-store / d2b-meta / d2b-hkeys. The - // parser must scan every element, not just the first after `--fs`. - let plan = test_spawn_plan_with_argv( - vec![ - "cloud-hypervisor".to_owned(), - "--fs".to_owned(), - "socket=/run/d2b/vms/work-aad/ro-store.sock,tag=ro-store".to_owned(), - "socket=/run/d2b/vms/work-aad/d2b-meta.sock,tag=d2b-meta".to_owned(), - "socket=/run/d2b/vms/work-aad/d2b-hkeys.sock,tag=d2b-hkeys".to_owned(), - "socket=/run/d2b/vms/work-aad/guest-control/d2b-gctl.sock,tag=d2b-gctl".to_owned(), - "--api-socket".to_owned(), - "/var/lib/d2b/vms/work-aad/work-aad.sock".to_owned(), - ], - "w1-cloud-hypervisor-runner", - ); - - assert_eq!( - cloud_hypervisor_guest_control_fs_socket_arg(&plan), - Some(PathBuf::from( - "/run/d2b/vms/work-aad/guest-control/d2b-gctl.sock" - )) - ); - } - - #[test] - fn guest_control_fs_socket_gated_on_ch_runner_policy() { - // Same d2b-gctl share element but a non-CH seccomp policy: the - // consumer ACL must never attach to a non-cloud-hypervisor runner. - let plan = test_spawn_plan_with_argv( - vec![ - "virtiofsd".to_owned(), - "socket=/run/d2b/vms/work-aad/guest-control/d2b-gctl.sock,tag=d2b-gctl".to_owned(), - ], - "w1-virtiofsd", - ); - - assert_eq!(cloud_hypervisor_guest_control_fs_socket_arg(&plan), None); - } - - #[test] - fn guest_control_fs_socket_absent_without_d2b_gctl_share() { - // A guest-control-disabled VM: CH runner with fs shares but no - // d2b-gctl tag. The grant must be a no-op. - let plan = test_spawn_plan_with_argv( - vec![ - "cloud-hypervisor".to_owned(), - "--fs".to_owned(), - "socket=/run/d2b/vms/work-aad/ro-store.sock,tag=ro-store".to_owned(), - "socket=/run/d2b/vms/work-aad/d2b-meta.sock,tag=d2b-meta".to_owned(), - ], - "w1-cloud-hypervisor-runner", - ); - - assert_eq!(cloud_hypervisor_guest_control_fs_socket_arg(&plan), None); - } - - #[test] - fn guest_control_fs_socket_rejects_relative_socket_path() { - let plan = test_spawn_plan_with_argv( - vec![ - "cloud-hypervisor".to_owned(), - "--fs".to_owned(), - "socket=relative/d2b-gctl.sock,tag=d2b-gctl".to_owned(), - ], - "w1-cloud-hypervisor-runner", - ); - - assert_eq!(cloud_hypervisor_guest_control_fs_socket_arg(&plan), None); - } - - #[test] - fn guest_control_fs_socket_parser_rejects_near_miss_tag() { - // A share whose tag merely CONTAINS `d2b-gctl` as a prefix - // (`tag=d2b-gctl-foo`) must not be mistaken for the token share: - // the parser does an exact comma-field match, never a substring - // match, so a future regression to substring matching would - // mis-grant the consumer ACL onto the wrong share. - let plan = test_spawn_plan_with_argv( - vec![ - "cloud-hypervisor".to_owned(), - "--fs".to_owned(), - "socket=/run/d2b/vms/work-aad/d2b-gctl-foo.sock,tag=d2b-gctl-foo".to_owned(), - ], - "w1-cloud-hypervisor-runner", - ); - - assert_eq!(cloud_hypervisor_guest_control_fs_socket_arg(&plan), None); - } - - #[test] - fn guest_control_fs_socket_grant_not_ready_for_absent_socket() { - // Hermetic: before the gctlfs virtiofsd creates the d2b-gctl - // socket, the consumer socket grant must report not-ready - // (Ok(false)) so the caller retries - never erroring (which would - // fail the cloud-hypervisor spawn outright) and never touching a - // foreign inode. This is the exact race the bounded retry thread - // exists to tolerate. - let dir = TestDir::new("gc-fs-acl"); - let socket = dir.join("d2b-gctl.sock"); - assert!(!socket.exists()); - assert_eq!( - grant_guest_control_fs_socket_acl_once(&socket, 4242), - Ok(false), - "absent socket must report not-ready", - ); - } - - #[test] - fn guest_control_fs_socket_acl_spec_pins_mask() { - // Regression guard: the `m::rw` mask token is load-bearing. The - // 0700 d2b-gctl socket's `mask::---` masks out the inherited - // CH-runner named entry, so the grant MUST pin the mask to `rw` - // (not the bare `u::rw` the vsock precedent uses). Dropping - // `,m::rw` would silently reintroduce the original EACCES/boot - // hang with every other unit test still green. The mask must not - // grant execute. - let spec = guest_control_fs_socket_acl_spec(1628571); - assert_eq!(spec, "u:1628571:rw,m::rw"); - assert!(spec.contains("m::rw"), "mask token missing: {spec}"); - assert!( - !spec.contains('x'), - "mask/entry must not grant execute: {spec}" - ); - } - - #[test] - fn guest_control_fs_detail_is_path_free() { - // The fs-share consumer error formatter must never leak a - // path-bearing legacy detail: it carries only closed-set class - // tokens (op/target-class/stage), the CH-runner consumer - // principal, the io::ErrorKind, and the numeric errno - same - // path-free contract as the daemon `guest_control_detail` twin. - let failure = SetfaclFailure { - stage: SetfaclStage::Apply, - errno_kind: std::io::ErrorKind::PermissionDenied, - raw_os_error: Some(13), - legacy_detail: "setfacl -m u:1628571:rw,m::rw on \ - /run/d2b/vms/corp-vm/guest-control/d2b-gctl.sock: denied" - .to_owned(), - }; - let detail = failure.guest_control_fs_detail("grant", "gctlfs-socket"); - assert!( - !detail.contains('/'), - "detail must not embed any path: {detail}" - ); - assert!( - !detail.contains("d2b-gctl.sock"), - "detail leaked socket name: {detail}" - ); - assert!(!detail.contains(":rw"), "detail leaked acl spec: {detail}"); - assert!( - !detail.contains("1628571"), - "detail leaked uid-by-value: {detail}" - ); - assert!( - detail.contains("gctlfs-socket"), - "missing target class: {detail}" - ); - assert!(detail.contains("stage=apply"), "missing stage: {detail}"); - assert!(detail.contains("errno=13"), "missing errno: {detail}"); - assert!( - detail.contains(GUEST_CONTROL_FS_CONSUMER_PRINCIPAL), - "missing consumer principal: {detail}" - ); + assert_eq!(cloud_hypervisor_component_session_socket_arg(&plan), None); } #[test] - fn guest_control_acl_diff_hash_is_path_free_and_stable() { - let h1 = guest_control_acl_diff_hash("grant", "vsock-socket", 0x10, 0x20); - let h2 = guest_control_acl_diff_hash("grant", "vsock-socket", 0x10, 0x20); - let h3 = guest_control_acl_diff_hash("revoke", "vsock-socket", 0x10, 0x20); + fn component_session_acl_diff_hash_is_path_free_and_stable() { + let h1 = component_session_acl_diff_hash("grant", "vsock-socket", 0x10, 0x20); + let h2 = component_session_acl_diff_hash("grant", "vsock-socket", 0x10, 0x20); + let h3 = component_session_acl_diff_hash("revoke", "vsock-socket", 0x10, 0x20); assert_eq!(h1, h2, "hash must be deterministic for identical inputs"); assert_ne!(h1, h3, "op must affect the hash"); assert!(h1.starts_with("sha256:")); @@ -5468,7 +5092,7 @@ mod tests { } #[test] - fn guest_control_acl_grant_not_ready_for_absent_socket() { + fn component_session_acl_grant_not_ready_for_absent_socket() { // Hermetic: before cloud-hypervisor creates the vsock socket, // the socket grant must report not-ready (Ok(false)) so the // caller retries - never erroring and never touching a foreign @@ -5477,7 +5101,7 @@ mod tests { let socket = dir.join("vsock.sock"); assert!(!socket.exists()); assert_eq!( - grant_guest_control_socket_acl_once(&socket), + grant_component_session_socket_acl_once(&socket), Ok(false), "absent socket must report not-ready", ); @@ -5488,13 +5112,13 @@ mod tests { // without invoking the host setfacl binary on real ancestors - // which a TestDir rooted under a non-world-x CI path (e.g. // `/home/runner`, mode 0750) would otherwise trigger. - revoke_guest_control_vsock_acl(&socket).expect("revoke of absent socket is a no-op"); + revoke_component_session_vsock_acl(&socket).expect("revoke of absent socket is a no-op"); } #[test] - fn setfacl_failure_guest_control_detail_is_path_free() { + fn setfacl_failure_component_session_detail_is_path_free() { // A path-bearing legacy detail must never leak through the - // guest-control formatter: it carries only closed-set class + // component-session formatter: it carries only closed-set class // tokens (op/target-class/stage), the daemon principal, the // io::ErrorKind, and the numeric errno. let failure = SetfaclFailure { @@ -5504,7 +5128,7 @@ mod tests { legacy_detail: "setfacl -m u:d2bd:rw on /var/lib/d2b/vms/corp-vm/vsock.sock: denied" .to_owned(), }; - let detail = failure.guest_control_detail("grant", "vsock-socket"); + let detail = failure.component_session_detail("grant", "vsock-socket"); assert!( !detail.contains('/'), "detail must not embed any path: {detail}" @@ -5521,7 +5145,7 @@ mod tests { assert!(detail.contains("stage=apply"), "missing stage: {detail}"); assert!(detail.contains("errno=13"), "missing errno: {detail}"); assert!( - detail.contains(GUEST_CONTROL_DAEMON_PRINCIPAL), + detail.contains(COMPONENT_SESSION_DAEMON_PRINCIPAL), "missing daemon principal: {detail}" ); // No-errno case renders a stable token. @@ -5531,7 +5155,7 @@ mod tests { raw_os_error: None, legacy_detail: "refusing setfacl on /secret/path".to_owned(), }; - let detail = mismatch.guest_control_detail("revoke", "state-dir"); + let detail = mismatch.component_session_detail("revoke", "state-dir"); assert!( !detail.contains('/'), "detail must not embed any path: {detail}" @@ -5701,7 +5325,8 @@ mod tests { umask: None, }; - let outcome = live_spawn_runner(&plan, Vec::new()).expect("spawn privileged test child"); + let outcome = + live_spawn_runner(&plan, Vec::new(), None).expect("spawn privileged test child"); let wait_status = nix::sys::wait::waitpid(nix::unistd::Pid::from_raw(outcome.pid), None) .expect("wait for test child"); assert!(matches!( diff --git a/packages/d2b-priv-broker/src/main.rs b/packages/d2b-broker/src/main.rs similarity index 95% rename from packages/d2b-priv-broker/src/main.rs rename to packages/d2b-broker/src/main.rs index ccc7150f86..0f25efc4ff 100644 --- a/packages/d2b-priv-broker/src/main.rs +++ b/packages/d2b-broker/src/main.rs @@ -1,6 +1,6 @@ use std::process::ExitCode; -use d2b_priv_broker::runtime::{RunError, parse_command, run}; +use d2b_broker::runtime::{RunError, parse_command, run}; fn main() -> ExitCode { // Enable RUST_LOG-driven env filter so the broker surfaces diff --git a/packages/d2b-priv-broker/src/ops/audit_op.rs b/packages/d2b-broker/src/ops/audit_op.rs similarity index 98% rename from packages/d2b-priv-broker/src/ops/audit_op.rs rename to packages/d2b-broker/src/ops/audit_op.rs index c4aa11895e..d23a714a60 100644 --- a/packages/d2b-priv-broker/src/ops/audit_op.rs +++ b/packages/d2b-broker/src/ops/audit_op.rs @@ -9,10 +9,7 @@ use std::io; use d2b_audit::{DurabilityEvidence, OperationIdentity, ZoneOperationKey}; use d2b_contracts_broker::broker_wire::{NftablesProjectionAction, RunnerAllocation}; -use d2b_contracts_resource::v3::{ - ResourceBundleGenerationId, - ResourceGeneration, -}; +use d2b_contracts_resource::v3::{ResourceBundleGenerationId, ResourceGeneration}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -431,14 +428,6 @@ pub enum OperationFields { read_only: bool, qmp_commands: Vec, }, - GuestControlSign { - vm_id: String, - role: String, - purpose: String, - transcript_len: usize, - peer_cid_present: bool, - capabilities_hash_present: bool, - }, ApplyNmUnmanaged { bundle_nm_intent_ref: String, scope_id: String, @@ -825,14 +814,6 @@ impl OperationFields { read_only: bool, qmp_commands: Vec, }), - "GuestControlSign" => parse_fields!(value => GuestControlSign { - vm_id: String, - role: String, - purpose: String, - transcript_len: usize, - peer_cid_present: bool, - capabilities_hash_present: bool, - }), "ApplyNmUnmanaged" => parse_fields!(value => ApplyNmUnmanaged { bundle_nm_intent_ref: String, scope_id: String, @@ -1484,18 +1465,6 @@ mod tests { env: "corp".to_owned(), } ); - roundtrip_test!( - guest_control_sign_round_trip, - "GuestControlSign", - OperationFields::GuestControlSign { - vm_id: "corp-vm".to_owned(), - role: "Health".to_owned(), - purpose: "Readiness".to_owned(), - transcript_len: 96, - peer_cid_present: true, - capabilities_hash_present: false, - } - ); roundtrip_test!( apply_nm_unmanaged_round_trip, "ApplyNmUnmanaged", diff --git a/packages/d2b-priv-broker/src/ops/cgroup.rs b/packages/d2b-broker/src/ops/cgroup.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/cgroup.rs rename to packages/d2b-broker/src/ops/cgroup.rs diff --git a/packages/d2b-priv-broker/src/ops/device.rs b/packages/d2b-broker/src/ops/device.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/device.rs rename to packages/d2b-broker/src/ops/device.rs diff --git a/packages/d2b-priv-broker/src/ops/disk_init.rs b/packages/d2b-broker/src/ops/disk_init.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/disk_init.rs rename to packages/d2b-broker/src/ops/disk_init.rs diff --git a/packages/d2b-priv-broker/src/ops/exec_reconcile.rs b/packages/d2b-broker/src/ops/exec_reconcile.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/exec_reconcile.rs rename to packages/d2b-broker/src/ops/exec_reconcile.rs diff --git a/packages/d2b-priv-broker/src/ops/gpu.rs b/packages/d2b-broker/src/ops/gpu.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/gpu.rs rename to packages/d2b-broker/src/ops/gpu.rs diff --git a/packages/d2b-priv-broker/src/ops/host_generation_handoff.rs b/packages/d2b-broker/src/ops/host_generation_handoff.rs similarity index 97% rename from packages/d2b-priv-broker/src/ops/host_generation_handoff.rs rename to packages/d2b-broker/src/ops/host_generation_handoff.rs index e064f420b7..499bbf749e 100644 --- a/packages/d2b-priv-broker/src/ops/host_generation_handoff.rs +++ b/packages/d2b-broker/src/ops/host_generation_handoff.rs @@ -82,9 +82,7 @@ impl HandoffEffect for SuccessfulHandoffEffect { request: &ApplyHostGenerationHandoff, ) -> Result { Ok( - if request.intent.activation_mode - == d2b_contracts_resource::v3::ActivationMode::Adopt - { + if request.intent.activation_mode == d2b_contracts_resource::v3::ActivationMode::Adopt { ActivationHelperOutcome::Adopted } else { ActivationHelperOutcome::Succeeded @@ -171,9 +169,9 @@ impl HandoffEffect for ActivationHelperEffect { /// Resolve and verify one private system artifact without activating it. /// -/// This is used during cutover admission, before the runner is allowed to -/// drain the daemon. The helper remains the sole authority for catalog, -/// store-path, and package-digest validation. +/// This is used before a host-generation handoff is activated. The helper +/// remains the sole authority for catalog, store-path, and package-digest +/// validation. pub fn validate_artifact_with_helper( helper_path: &Path, artifact_id: &ArtifactId, @@ -401,11 +399,7 @@ mod tests { use d2b_contracts_broker::host_generation::{ HandoffCallerRole, HostGenerationHandoffIntent, SourceGenerationCompatibilityFloorV1, }; - use d2b_contracts_resource::v3::{ - ActivationMode, - ArtifactId, - ResourceRef, -}; + use d2b_contracts_resource::v3::{ActivationMode, ArtifactId, ResourceRef}; fn request() -> ApplyHostGenerationHandoff { let target = ResourceRef::parse("Host/host-system").unwrap(); diff --git a/packages/d2b-priv-broker/src/ops/hosts.rs b/packages/d2b-broker/src/ops/hosts.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/hosts.rs rename to packages/d2b-broker/src/ops/hosts.rs diff --git a/packages/d2b-priv-broker/src/ops/media.rs b/packages/d2b-broker/src/ops/media.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/media.rs rename to packages/d2b-broker/src/ops/media.rs diff --git a/packages/d2b-priv-broker/src/ops/mod.rs b/packages/d2b-broker/src/ops/mod.rs similarity index 98% rename from packages/d2b-priv-broker/src/ops/mod.rs rename to packages/d2b-broker/src/ops/mod.rs index 67d36988c7..7ecd57c12d 100644 --- a/packages/d2b-priv-broker/src/ops/mod.rs +++ b/packages/d2b-broker/src/ops/mod.rs @@ -58,8 +58,6 @@ pub mod exec_reconcile; // Audit-helper introduced by s2; reusable by s1/s3/s4 going forward. pub mod audit_op; -// Operation-scoped cutover quarantine and finalization markers. -pub mod cutover_artifacts; // Broker-owned source-to-target NixOS generation handoff journal and replay. pub mod host_generation_handoff; diff --git a/packages/d2b-priv-broker/src/ops/modprobe.rs b/packages/d2b-broker/src/ops/modprobe.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/modprobe.rs rename to packages/d2b-broker/src/ops/modprobe.rs diff --git a/packages/d2b-priv-broker/src/ops/network.rs b/packages/d2b-broker/src/ops/network.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/network.rs rename to packages/d2b-broker/src/ops/network.rs index 3825d5cea7..fee23f62f7 100644 --- a/packages/d2b-priv-broker/src/ops/network.rs +++ b/packages/d2b-broker/src/ops/network.rs @@ -641,10 +641,7 @@ fn digest_parts(parts: &[&[u8]]) -> String { mod tests { use super::*; use d2b_contracts_resource::v3::IfName; - use d2b_contracts_resource::v3::{ - ResourceGeneration, - ResourceUid, -}; + use d2b_contracts_resource::v3::{ResourceGeneration, ResourceUid}; use std::cell::Cell; struct FakeBridge { diff --git a/packages/d2b-priv-broker/src/ops/nft.rs b/packages/d2b-broker/src/ops/nft.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/nft.rs rename to packages/d2b-broker/src/ops/nft.rs index b147b91ce6..334465fb48 100644 --- a/packages/d2b-priv-broker/src/ops/nft.rs +++ b/packages/d2b-broker/src/ops/nft.rs @@ -1164,11 +1164,10 @@ mod tests { ), scope_id: d2b_contracts::types::ScopeId::new("scope:opaque"), action: d2b_contracts_broker::broker_wire::NftablesProjectionAction::Apply, - expected_generation_id: - d2b_contracts_resource::v3::ResourceBundleGenerationId::parse( - "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - ) - .unwrap(), + expected_generation_id: d2b_contracts_resource::v3::ResourceBundleGenerationId::parse( + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ) + .unwrap(), desired_hash: None, tracing_span_id: None, }; diff --git a/packages/d2b-priv-broker/src/ops/nm.rs b/packages/d2b-broker/src/ops/nm.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/nm.rs rename to packages/d2b-broker/src/ops/nm.rs index 44afd393d3..c49d3de7b2 100644 --- a/packages/d2b-priv-broker/src/ops/nm.rs +++ b/packages/d2b-broker/src/ops/nm.rs @@ -92,7 +92,7 @@ impl From for ApplyNmError { pub fn render_nm_conf(entries: &[NmUnmanagedEntry]) -> String { let mut out = String::new(); out.push_str("# d2b-managed begin\n"); - out.push_str("# Generated by d2b-priv-broker; do not edit by hand.\n"); + out.push_str("# Generated by d2b-broker; do not edit by hand.\n"); out.push_str("[keyfile]\n"); let unmanaged_devices: Vec = entries .iter() diff --git a/packages/d2b-priv-broker/src/ops/pidfd.rs b/packages/d2b-broker/src/ops/pidfd.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/pidfd.rs rename to packages/d2b-broker/src/ops/pidfd.rs diff --git a/packages/d2b-priv-broker/src/ops/route.rs b/packages/d2b-broker/src/ops/route.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/route.rs rename to packages/d2b-broker/src/ops/route.rs diff --git a/packages/d2b-priv-broker/src/ops/security_key.rs b/packages/d2b-broker/src/ops/security_key.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/security_key.rs rename to packages/d2b-broker/src/ops/security_key.rs diff --git a/packages/d2b-priv-broker/src/ops/spawn_runner.rs b/packages/d2b-broker/src/ops/spawn_runner.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/spawn_runner.rs rename to packages/d2b-broker/src/ops/spawn_runner.rs diff --git a/packages/d2b-priv-broker/src/ops/state_dir.rs b/packages/d2b-broker/src/ops/state_dir.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/state_dir.rs rename to packages/d2b-broker/src/ops/state_dir.rs diff --git a/packages/d2b-priv-broker/src/ops/storage_contract.rs b/packages/d2b-broker/src/ops/storage_contract.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/storage_contract.rs rename to packages/d2b-broker/src/ops/storage_contract.rs index f2bc39be37..094dd7bd75 100644 --- a/packages/d2b-priv-broker/src/ops/storage_contract.rs +++ b/packages/d2b-broker/src/ops/storage_contract.rs @@ -634,7 +634,6 @@ mod tests { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: Some("storage.json".to_owned()), sync_path: Some("sync.json".to_owned()), allocator_path: None, @@ -658,7 +657,6 @@ mod tests { minimal_host(), ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: Vec::new(), }, Some(storage_contract), @@ -685,8 +683,8 @@ mod tests { mode: "0750".to_owned(), access_acl: Vec::new(), default_acl: Vec::new(), - creator: actor(ActorKind::Broker, "d2b-priv-broker"), - writers: vec![actor(ActorKind::Broker, "d2b-priv-broker")], + creator: actor(ActorKind::Broker, "d2b-broker"), + writers: vec![actor(ActorKind::Broker, "d2b-broker")], readers: vec![actor(ActorKind::Daemon, "d2bd")], cleanup_policy: CleanupPolicy::Boot, repair_policy: RepairPolicy::BrokerReconcile, diff --git a/packages/d2b-priv-broker/src/ops/store_sync.rs b/packages/d2b-broker/src/ops/store_sync.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_sync.rs rename to packages/d2b-broker/src/ops/store_sync.rs diff --git a/packages/d2b-priv-broker/src/ops/store_sync_audit.rs b/packages/d2b-broker/src/ops/store_sync_audit.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_sync_audit.rs rename to packages/d2b-broker/src/ops/store_sync_audit.rs diff --git a/packages/d2b-priv-broker/src/ops/store_sync_export.rs b/packages/d2b-broker/src/ops/store_sync_export.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_sync_export.rs rename to packages/d2b-broker/src/ops/store_sync_export.rs diff --git a/packages/d2b-priv-broker/src/ops/store_verify.rs b/packages/d2b-broker/src/ops/store_verify.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_verify.rs rename to packages/d2b-broker/src/ops/store_verify.rs diff --git a/packages/d2b-priv-broker/src/ops/store_view_farm.rs b/packages/d2b-broker/src/ops/store_view_farm.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_view_farm.rs rename to packages/d2b-broker/src/ops/store_view_farm.rs diff --git a/packages/d2b-priv-broker/src/ops/store_view_posture.rs b/packages/d2b-broker/src/ops/store_view_posture.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_view_posture.rs rename to packages/d2b-broker/src/ops/store_view_posture.rs diff --git a/packages/d2b-priv-broker/src/ops/swtpm_dir.rs b/packages/d2b-broker/src/ops/swtpm_dir.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/swtpm_dir.rs rename to packages/d2b-broker/src/ops/swtpm_dir.rs index 196372a80e..6453a6c57e 100644 --- a/packages/d2b-priv-broker/src/ops/swtpm_dir.rs +++ b/packages/d2b-broker/src/ops/swtpm_dir.rs @@ -876,7 +876,6 @@ mod tests { .build(); let processes = ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: vec![VmProcessDag { vm: "corp-vm".to_owned(), workload_identity: None, @@ -916,7 +915,6 @@ mod tests { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, diff --git a/packages/d2b-priv-broker/src/ops/swtpm_migration.rs b/packages/d2b-broker/src/ops/swtpm_migration.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/swtpm_migration.rs rename to packages/d2b-broker/src/ops/swtpm_migration.rs diff --git a/packages/d2b-priv-broker/src/ops/sysctl.rs b/packages/d2b-broker/src/ops/sysctl.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/sysctl.rs rename to packages/d2b-broker/src/ops/sysctl.rs diff --git a/packages/d2b-priv-broker/src/ops/systemd.rs b/packages/d2b-broker/src/ops/systemd.rs similarity index 92% rename from packages/d2b-priv-broker/src/ops/systemd.rs rename to packages/d2b-broker/src/ops/systemd.rs index 63ef9ef200..ef3dcc7efe 100644 --- a/packages/d2b-priv-broker/src/ops/systemd.rs +++ b/packages/d2b-broker/src/ops/systemd.rs @@ -30,7 +30,6 @@ const SYSTEMD_UNIT_INTERFACE: &str = "org.freedesktop.systemd1.Unit"; const SYSTEMD_METHOD_TIMEOUT: Duration = Duration::from_secs(5); const IDENTITY_READY_TIMEOUT: Duration = Duration::from_secs(5); const IDENTITY_RETRY_INTERVAL: Duration = Duration::from_millis(20); -const CUTOVER_DAEMON_UNIT: &str = "d2bd.service"; /// Closed failures from the broker-owned systemd effect owner. #[derive(Debug, Clone, PartialEq, Eq)] @@ -113,9 +112,8 @@ fn validate_request( { return Err(SystemdError::BundleIntent); } - let expected_execution = - d2b_contracts_resource::v3::ResourceRef::parse(&intent.execution_ref) - .map_err(|_| SystemdError::IdentityMismatch)?; + let expected_execution = d2b_contracts_resource::v3::ResourceRef::parse(&intent.execution_ref) + .map_err(|_| SystemdError::IdentityMismatch)?; if request .execution_ref .as_ref() @@ -393,6 +391,7 @@ fn read_identity( template_identity: request.template_identity, generation: request.generation, bundle_content_identity: request.bundle_content_identity.clone(), + guest_execution: request.guest_execution.clone(), })) } @@ -545,59 +544,12 @@ pub fn stop( } } -/// Stop the fixed daemon unit for the cutover drain boundary. -/// -/// This is deliberately not a general unit operation: the unit name is -/// compile-time fixed and the broker verifies the unit reaches an inactive -/// state before reporting drain completion. -pub fn stop_cutover_daemon() -> Result<(), SystemdError> { - let connection = system_connection()?; - let manager = manager_proxy(&connection)?; - let unit_path = match unit_proxy(&manager, CUTOVER_DAEMON_UNIT) { - Ok(path) => path, - Err(SystemdError::BundleIntent) => return Ok(()), - Err(error) => return Err(error), - }; - let unit = Proxy::new( - &connection, - SYSTEMD_DESTINATION, - unit_path.as_str(), - SYSTEMD_UNIT_INTERFACE, - ) - .map_err(|_| SystemdError::Query)?; - let active_state: String = unit - .get_property("ActiveState") - .map_err(|_| SystemdError::Query)?; - if active_state == "inactive" || active_state == "failed" { - return Ok(()); - } - manager - .call_method("StopUnit", &(CUTOVER_DAEMON_UNIT, "replace")) - .map_err(|_| SystemdError::Stop)?; - let deadline = Instant::now() + IDENTITY_READY_TIMEOUT; - loop { - let state: String = unit - .get_property("ActiveState") - .map_err(|_| SystemdError::Query)?; - if state == "inactive" || state == "failed" { - return Ok(()); - } - if Instant::now() >= deadline { - return Err(SystemdError::Timeout); - } - thread::sleep(IDENTITY_RETRY_INTERVAL); - } -} - #[cfg(test)] mod tests { use super::*; use d2b_contracts::types::{BundleOpId, RoleId, VmId}; use d2b_contracts_broker::broker_wire::{RunnerRole, SystemdUnitRequest}; - use d2b_contracts_resource::v3::{ - ResourceRef, - ResourceUid, -}; + use d2b_contracts_resource::v3::{ResourceRef, ResourceUid}; fn request() -> SystemdUnitRequest { SystemdUnitRequest { @@ -614,6 +566,7 @@ mod tests { template_identity: [2; 32], generation: 3, domain: SystemdUnitDomain::System, + guest_execution: None, sandbox_plan: None, tracing_span_id: None, } diff --git a/packages/d2b-priv-broker/src/ops/tap.rs b/packages/d2b-broker/src/ops/tap.rs similarity index 98% rename from packages/d2b-priv-broker/src/ops/tap.rs rename to packages/d2b-broker/src/ops/tap.rs index 5c2cfb2651..708accf273 100644 --- a/packages/d2b-priv-broker/src/ops/tap.rs +++ b/packages/d2b-broker/src/ops/tap.rs @@ -969,9 +969,7 @@ pub fn fake_backend() -> FakeBackend { #[cfg(test)] mod tests { use super::*; - use d2b_contracts_resource::v3::{ - IfName as BundleIfName, -}; + use d2b_contracts_resource::v3::IfName as BundleIfName; use d2b_core::bundle::{Bundle, BundleGeneration}; use d2b_core::host::{ BridgePortFlags as HostBridgePortFlags, HostJson as BundleHostJson, IfNameMapping, @@ -989,7 +987,6 @@ mod tests { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -1100,13 +1097,12 @@ mod tests { ch: None, firewall_coexistence_policy: None, }; - let manifest = ManifestV04::from_slice(br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/run/d2b/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/run/d2b/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/run/d2b/corp-vm-vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"guestControl":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#.as_slice()).expect("manifest"); + let manifest = ManifestV04::from_slice(br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/run/d2b/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/run/d2b/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/run/d2b/corp-vm-vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#.as_slice()).expect("manifest"); BundleResolver::from_artifacts( bundle, host, ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: Vec::new(), }, manifest, diff --git a/packages/d2b-priv-broker/src/ops/usbip_firewall.rs b/packages/d2b-broker/src/ops/usbip_firewall.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/usbip_firewall.rs rename to packages/d2b-broker/src/ops/usbip_firewall.rs diff --git a/packages/d2b-priv-broker/src/ops/usbip_host.rs b/packages/d2b-broker/src/ops/usbip_host.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/usbip_host.rs rename to packages/d2b-broker/src/ops/usbip_host.rs diff --git a/packages/d2b-priv-broker/src/ops/usbip_lock.rs b/packages/d2b-broker/src/ops/usbip_lock.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/usbip_lock.rs rename to packages/d2b-broker/src/ops/usbip_lock.rs diff --git a/packages/d2b-priv-broker/src/ops/zone_store.rs b/packages/d2b-broker/src/ops/zone_store.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/zone_store.rs rename to packages/d2b-broker/src/ops/zone_store.rs index cd1a0b239a..f05e14d436 100644 --- a/packages/d2b-priv-broker/src/ops/zone_store.rs +++ b/packages/d2b-broker/src/ops/zone_store.rs @@ -16,13 +16,11 @@ use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; use d2b_contracts_broker::broker_wire::{OpenZoneStoreResponse, ZoneStoreDisposition}; -use d2b_contracts_resource::v3::{ - storage::{ +use d2b_contracts_resource::v3::storage::{ ZoneStoreAuxiliaryDirectory, ZoneStoreDescriptorPublicationRequirement, ZoneStoreDirectoryRepairOwner, ZoneStoreFilesystemRequirement, ZoneStoreFsyncRequirement, ZoneStoreId, ZoneStoreLockingRequirement, ZoneStorePrincipal, ZoneStoreReplacementDetection, ZoneStoreReplacementPublicationRequirement, ZoneStoreStorageRow, -}, }; use d2b_core::bundle_resolver::BundleResolver; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; diff --git a/packages/d2b-priv-broker/src/protocol.rs b/packages/d2b-broker/src/protocol.rs similarity index 100% rename from packages/d2b-priv-broker/src/protocol.rs rename to packages/d2b-broker/src/protocol.rs diff --git a/packages/d2b-priv-broker/src/runtime.rs b/packages/d2b-broker/src/runtime.rs similarity index 88% rename from packages/d2b-priv-broker/src/runtime.rs rename to packages/d2b-broker/src/runtime.rs index 9fa20018b2..dde432d612 100644 --- a/packages/d2b-priv-broker/src/runtime.rs +++ b/packages/d2b-broker/src/runtime.rs @@ -2,17 +2,17 @@ use std::env; use std::fs; use std::io; #[cfg(not(feature = "layer1-bootstrap"))] -use std::io::{Read, Write}; +use std::io::Read; use std::os::fd::{AsFd, AsRawFd, OwnedFd}; use std::os::unix::fs::MetadataExt; #[cfg(not(feature = "layer1-bootstrap"))] -use std::os::unix::fs::{FileTypeExt, OpenOptionsExt, PermissionsExt}; +use std::os::unix::fs::FileTypeExt; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; #[cfg(not(feature = "layer1-bootstrap"))] use std::{ - collections::{BTreeMap, HashMap}, + collections::HashMap, sync::OnceLock, time::{SystemTime, UNIX_EPOCH}, }; @@ -26,12 +26,9 @@ use nix::libc; use nix::sys::socket::{AddressFamily, SockType, socketpair}; use nix::sys::socket::{SockFlag, accept4}; #[cfg(not(feature = "layer1-bootstrap"))] -use nix::unistd::{dup, geteuid}; -#[cfg(not(feature = "layer1-bootstrap"))] -use serde::{Deserialize, Serialize}; +use nix::unistd::dup; use serde_json::Value; -#[cfg(not(feature = "layer1-bootstrap"))] -use sha2::Sha256; +use sha2::{Digest, Sha256}; #[cfg(not(feature = "layer1-bootstrap"))] use tracing::info; use tracing::warn; @@ -56,10 +53,12 @@ use crate::protocol::{ use crate::bootstrap::manifest as manifest_api; #[cfg(feature = "layer1-bootstrap")] use crate::bootstrap::wire::{BrokerRequest, BrokerResponse, CallerRole, RequestEnvelope}; +#[cfg(feature = "layer1-bootstrap")] +use d2b_contracts_broker::broker_wire::BrokerProfile; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_contracts_broker::broker_wire::{ - AuditJoinContext, BrokerCallerRole as CallerRole, BrokerRequest, - BrokerRequestEnvelope as RequestEnvelope, BrokerResponse, CanonicalAuditDigest, + AuditJoinContext, BrokerCallerRole as CallerRole, BrokerProfile, BrokerRequest, + BrokerRequestEnvelope as RequestEnvelope, BrokerResponse, CanonicalAuditDigest, RunnerRole, }; #[cfg(feature = "layer1-bootstrap")] type AuditJoinContext = (); @@ -75,10 +74,12 @@ use d2b_realm_core::{RealmIdentityConfigJson, RealmIdentityConfigSummary}; /// `D2B_BROKER_SOCKET_PATH`) to override the path used in non-activated /// (test / legacy) mode. const DEFAULT_SOCKET_PATH: &str = "/run/d2b/priv.sock"; +const DEFAULT_GUEST_SOCKET_PATH: &str = "/run/d2b/guest-broker.sock"; /// Audit records land under /// `/var/lib/d2b/audit/broker-.jsonl` (no more legacy /// single `broker-audit.log` file). Override via `--audit-dir`. const DEFAULT_AUDIT_DIR: &str = "/var/lib/d2b/audit"; +const DEFAULT_GUEST_AUDIT_DIR: &str = "/var/lib/d2b/guest-audit"; /// Default audit retention. Matches the docs claim in /// `docs/reference/daemon-api.md` "Audit" and `AGENTS.md` "Control /// plane". Override via `--audit-retention-days` (broker flag) or the @@ -86,302 +87,21 @@ const DEFAULT_AUDIT_DIR: &str = "/var/lib/d2b/audit"; /// to disable pruning. const DEFAULT_AUDIT_RETENTION_DAYS: u32 = 30; const DEFAULT_BUNDLE_PATH: &str = "/var/lib/d2b/current-bundle/manifest.json"; +const DEFAULT_GUEST_BUNDLE_PATH: &str = "/etc/d2b/guest-bundle.json"; const DEFAULT_REALM_CONTROLLERS_PATH: &str = "/etc/d2b/realm-controllers.json"; +const DEFAULT_GUEST_REALM_CONTROLLERS_PATH: &str = "/etc/d2b/guest-realm-controllers.json"; const DEFAULT_REALM_IDENTITY_PATH: &str = "/etc/d2b/realm-identity.json"; +const DEFAULT_GUEST_REALM_IDENTITY_PATH: &str = "/etc/d2b/guest-realm-identity.json"; const DEFAULT_STATE_DIR: &str = "/var/lib/d2b"; +const DEFAULT_GUEST_STATE_DIR: &str = "/var/lib/d2b/guest-broker"; const DEFAULT_ACTIVATION_HELPER_PATH: &str = "/run/current-system/sw/bin/d2b-activation-helper"; -const DEFAULT_CUTOVER_RUNNER_PATH: &str = "/run/current-system/sw/bin/d2b-cutover-runner"; -const DEFAULT_CUTOVER_SOCKET_ROOT: &str = "/run/d2b"; const CAPABILITIES: &[&str] = &[ "Hello", "ValidateBundle", "ExportBrokerAudit", "MigrateLegacySwtpmState", "ApplyHostGenerationHandoff", - "LaunchCutoverRunner", - "CutoverAudit", - "CutoverEffect", ]; -#[cfg(not(feature = "layer1-bootstrap"))] -static CUTOVER_CAPABILITY_LOCK: OnceLock> = OnceLock::new(); -const CUTOVER_CAPABILITY_FILE: &str = "cutover-capabilities.json"; - -#[cfg(not(feature = "layer1-bootstrap"))] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -struct CutoverCapabilityBinding { - capability_digest: CanonicalAuditDigest, - expires_at_ms: u64, - #[serde(default)] - drained: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - runner_pid: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - runner_start_time_ticks: Option, -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_capability_lock() -> &'static Mutex<()> { - CUTOVER_CAPABILITY_LOCK.get_or_init(|| Mutex::new(())) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_capability_path(config: &ServerConfig) -> PathBuf { - config.state_dir.join(CUTOVER_CAPABILITY_FILE) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn load_cutover_capabilities( - config: &ServerConfig, -) -> Result, BrokerError> { - let path = cutover_capability_path(config); - let owner = if config.test_mode { - geteuid().as_raw() - } else { - 0 - }; - match fs::symlink_metadata(&path) { - Ok(metadata) - if metadata.is_file() - && metadata.uid() == owner - && metadata.mode() & 0o777 == 0o600 => {} - Ok(_) => { - return Err(BrokerError::Protocol( - "cutover capability registry has foreign ownership".to_owned(), - )); - } - Err(error) if error.kind() == io::ErrorKind::NotFound => { - return Ok(BTreeMap::new()); - } - Err(error) => { - return Err(BrokerError::Protocol(format!( - "cutover capability registry read: {error}" - ))); - } - } - let bytes = fs::read(&path).map_err(|error| { - BrokerError::Protocol(format!("cutover capability registry read: {error}")) - })?; - serde_json::from_slice(&bytes) - .map_err(|_| BrokerError::Protocol("cutover capability registry malformed".to_owned())) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn persist_cutover_capabilities( - config: &ServerConfig, - capabilities: &BTreeMap, -) -> Result<(), BrokerError> { - if !config.state_dir.exists() { - fs::create_dir_all(&config.state_dir).map_err(|error| { - BrokerError::Protocol(format!("cutover capability state directory: {error}")) - })?; - fs::set_permissions(&config.state_dir, fs::Permissions::from_mode(0o700)).map_err( - |error| BrokerError::Protocol(format!("cutover capability state mode: {error}")), - )?; - } - let metadata = fs::symlink_metadata(&config.state_dir).map_err(|error| { - BrokerError::Protocol(format!("cutover capability state directory: {error}")) - })?; - let owner = if config.test_mode { - geteuid().as_raw() - } else { - 0 - }; - if !metadata.is_dir() || metadata.uid() != owner { - return Err(BrokerError::Protocol( - "cutover capability state directory has foreign ownership".to_owned(), - )); - } - let path = cutover_capability_path(config); - let temp = config.state_dir.join(format!( - "{CUTOVER_CAPABILITY_FILE}.{}.tmp", - std::process::id() - )); - let mut file = fs::OpenOptions::new() - .create_new(true) - .write(true) - .mode(0o600) - .open(&temp) - .map_err(|error| { - BrokerError::Protocol(format!("cutover capability registry create: {error}")) - })?; - let bytes = serde_json::to_vec(capabilities) - .map_err(|_| BrokerError::Protocol("cutover capability registry encode".to_owned()))?; - file.write_all(&bytes) - .and_then(|_| file.sync_all()) - .map_err(|error| { - BrokerError::Protocol(format!("cutover capability registry sync: {error}")) - })?; - drop(file); - if let Ok(metadata) = fs::symlink_metadata(&path) - && (!metadata.is_file() || metadata.uid() != owner || metadata.mode() & 0o777 != 0o600) - { - return Err(BrokerError::Protocol( - "cutover capability registry has foreign ownership".to_owned(), - )); - } - fs::rename(&temp, &path) - .and_then(|_| fs::File::open(&config.state_dir)?.sync_all()) - .map_err(|error| { - BrokerError::Protocol(format!("cutover capability registry commit: {error}")) - })?; - Ok(()) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn register_cutover_capability( - config: &ServerConfig, - operation_id: &d2b_contracts::types::BundleOpId, - capability_digest: &CanonicalAuditDigest, - expires_at_ms: u64, -) -> Result<(), BrokerError> { - let _guard = cutover_capability_lock() - .lock() - .map_err(|_| BrokerError::Protocol("cutover capability registry poisoned".to_owned()))?; - let mut capabilities = load_cutover_capabilities(config)?; - let mut drained = false; - if let Some(binding) = capabilities.get(operation_id.as_str()).cloned() { - let replace = match cutover_capability_runner_is_live(&binding)? { - Some(runner_is_live) => !runner_is_live, - None => audit_timestamp_ms() > u128::from(binding.expires_at_ms), - }; - if !replace { - return Err(BrokerError::Protocol( - "cutover operation capability already registered".to_owned(), - )); - } - drained = binding.drained; - capabilities.remove(operation_id.as_str()); - } - capabilities.insert( - operation_id.to_string(), - CutoverCapabilityBinding { - capability_digest: capability_digest.clone(), - expires_at_ms, - drained, - runner_pid: None, - runner_start_time_ticks: None, - }, - ); - persist_cutover_capabilities(config, &capabilities) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn bind_cutover_capability_runner( - config: &ServerConfig, - operation_id: &d2b_contracts::types::BundleOpId, - pid: i32, - start_time_ticks: u64, -) -> Result<(), BrokerError> { - if pid <= 0 || start_time_ticks == 0 { - return Err(BrokerError::Protocol( - "cutover runner identity is invalid".to_owned(), - )); - } - let _guard = cutover_capability_lock() - .lock() - .map_err(|_| BrokerError::Protocol("cutover capability registry poisoned".to_owned()))?; - let mut capabilities = load_cutover_capabilities(config)?; - let binding = capabilities - .get_mut(operation_id.as_str()) - .ok_or_else(|| BrokerError::Protocol("cutover capability binding missing".to_owned()))?; - binding.runner_pid = Some(pid); - binding.runner_start_time_ticks = Some(start_time_ticks); - persist_cutover_capabilities(config, &capabilities) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_capability_runner_is_live( - binding: &CutoverCapabilityBinding, -) -> Result, BrokerError> { - let (Some(pid), Some(expected_start_time_ticks)) = - (binding.runner_pid, binding.runner_start_time_ticks) - else { - return Ok(None); - }; - if pid <= 0 || expected_start_time_ticks == 0 { - return Ok(Some(false)); - } - Ok(Some( - read_proc_start_time_ticks(pid)?.is_some_and(|actual| actual == expected_start_time_ticks), - )) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn remove_cutover_capability( - config: &ServerConfig, - operation_id: &d2b_contracts::types::BundleOpId, -) -> Result<(), BrokerError> { - let _guard = cutover_capability_lock() - .lock() - .map_err(|_| BrokerError::Protocol("cutover capability registry poisoned".to_owned()))?; - let mut capabilities = load_cutover_capabilities(config)?; - if capabilities.remove(operation_id.as_str()).is_some() { - persist_cutover_capabilities(config, &capabilities)?; - } - Ok(()) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn mark_cutover_capability_drained( - config: &ServerConfig, - operation_id: &d2b_contracts::types::BundleOpId, -) -> Result<(), BrokerError> { - let _guard = cutover_capability_lock() - .lock() - .map_err(|_| BrokerError::Protocol("cutover capability registry poisoned".to_owned()))?; - let mut capabilities = load_cutover_capabilities(config)?; - let binding = capabilities.get_mut(operation_id.as_str()).ok_or({ - BrokerError::PeerCredentialRefused { - operation: "CutoverEffect", - } - })?; - binding.drained = true; - persist_cutover_capabilities(config, &capabilities) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_window_active(config: &ServerConfig) -> Result { - let _guard = cutover_capability_lock() - .lock() - .map_err(|_| BrokerError::Protocol("cutover capability registry poisoned".to_owned()))?; - let capabilities = load_cutover_capabilities(config)?; - Ok(capabilities.values().any(|binding| { - binding.drained && audit_timestamp_ms() <= u128::from(binding.expires_at_ms) - })) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_relaunch_allowed( - config: &ServerConfig, - request: &d2b_contracts_broker::broker_wire::BrokerRequest, -) -> Result { - let d2b_contracts_broker::broker_wire::BrokerRequest::LaunchCutoverRunner(request) = request else { - return Ok(false); - }; - let capabilities = load_cutover_capabilities(config)?; - let Some(binding) = capabilities.get(request.operation_id.as_str()) else { - return Ok(false); - }; - Ok(cutover_capability_runner_is_live(binding)?.is_some_and(|runner_is_live| !runner_is_live)) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_capability_matches( - config: &ServerConfig, - operation_id: &d2b_contracts::types::BundleOpId, - capability_digest: &CanonicalAuditDigest, -) -> Result { - let _guard = cutover_capability_lock() - .lock() - .map_err(|_| BrokerError::Protocol("cutover capability registry poisoned".to_owned()))?; - let capabilities = load_cutover_capabilities(config)?; - Ok(capabilities - .get(operation_id.as_str()) - .is_some_and(|binding| { - binding.capability_digest == *capability_digest - && audit_timestamp_ms() <= u128::from(binding.expires_at_ms) - })) -} const DEFAULT_IPC_REQUESTS_PER_UID_PER_SECOND: u32 = 512; const IPC_RATE_LIMIT_WINDOW: Duration = Duration::from_secs(1); const DEFAULT_IPC_RATE_LIMIT_MAX_BUCKETS: usize = 4096; @@ -389,6 +109,11 @@ const MAX_MODULE_NAME_LEN: usize = 64; #[derive(Debug, Clone)] pub struct ServerConfig { + /// Fixed process-start authority profile. Requests cannot change it. + pub profile: BrokerProfile, + /// Stable authority label binding this process instance's state, socket, + /// audit root, and caller identity. + pub authority_id: String, pub socket_path: PathBuf, pub audit_dir: PathBuf, pub audit_retention_days: u32, @@ -409,9 +134,6 @@ pub struct ServerConfig { /// Trusted target-local activation helper. The daemon never supplies /// this path on the wire. pub activation_helper_path: PathBuf, - /// Trusted one-shot cutover runner. The daemon never supplies this path - /// on the wire and the runner is not a persistent systemd unit. - pub cutover_runner_path: PathBuf, pub d2bd_uid: u32, pub d2bd_gid: u32, /// Directory for the StoreSync-only observability JSONL export @@ -425,418 +147,10 @@ pub struct ServerConfig { pub test_mode: bool, } -#[cfg(all(test, not(feature = "layer1-bootstrap")))] -mod cutover_audit_tests { - use super::*; - use d2b_contracts_broker::broker_wire::{ - BrokerCallerRole, BrokerRequest, CanonicalAuditDigest, CutoverAuditRequest, - CutoverAuditTransition, CutoverEffectAuthority, CutoverEffectKind, CutoverEffectRequest, - CutoverReplayClass, - }; - use d2b_contracts::types::BundleOpId; - use nix::unistd::Gid; - - fn request() -> BrokerRequest { - BrokerRequest::CutoverAudit(CutoverAuditRequest { - operation_id: BundleOpId::new("op-audit-runtime"), - phase: 3, - transition: CutoverAuditTransition::HoldRequested, - request_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)) - .expect("request digest"), - reason_digest: None, - }) - } - - fn config(root: &Path) -> ServerConfig { - ServerConfig { - socket_path: root.join("broker.sock"), - audit_dir: root.join("audit"), - audit_retention_days: 30, - bundle_path: root.join("bundle.json"), - realm_controllers_path: root.join("realm-controllers.json"), - realm_identity_path: root.join("realm-identity.json"), - state_dir: root.join("state"), - activation_helper_path: root.join("activation-helper"), - cutover_runner_path: root.join("cutover-runner"), - d2bd_uid: 1000, - d2bd_gid: Gid::current().as_raw(), - store_sync_export_dir: root.join("store-sync"), - test_mode: true, - } - } - - fn runner_role(operation_id: BundleOpId) -> BrokerCallerRole { - let capability_digest = CanonicalAuditDigest::parse("sha256:".to_owned() + &"b".repeat(64)) - .expect("capability digest"); - BrokerCallerRole::CutoverRunner { - operation_id, - capability_digest, - } - } - - #[test] - fn adapted_runner_audit_writes_durable_record() { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-audit-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let audit_log = - AuditLog::open(&config.audit_dir, config.d2bd_gid, true, 30).expect("audit log"); - let request = request(); - let operation_id = match &request { - BrokerRequest::CutoverAudit(request) => request.operation_id.clone(), - _ => unreachable!(), - }; - let role = runner_role(operation_id.clone()); - let capability_digest = match &role { - BrokerCallerRole::CutoverRunner { - capability_digest, .. - } => capability_digest, - _ => unreachable!(), - }; - register_cutover_capability(&config, &operation_id, capability_digest, u64::MAX) - .expect("register test capability"); - let context = DispatchAuditContext::from_request_with_join(&request, 123, &role, None) - .expect("audit context"); - let result = dispatch_request_with_request_fds( - request, - 0, - 0, - role, - &context, - &config, - &audit_log, - None, - Vec::new(), - ) - .expect("runner audit dispatch"); - assert!(matches!(result.response, BrokerResponse::CutoverAudit(_))); - let audit = fs::read_to_string(audit_log.current_daily_path()).expect("audit record"); - assert!(audit.contains("CutoverAudit")); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn admin_peer_cannot_use_runner_audit_operation() { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-audit-deny-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let audit_log = - AuditLog::open(&config.audit_dir, config.d2bd_gid, true, 30).expect("audit log"); - let request = request(); - let context = DispatchAuditContext::from_request_with_join( - &request, - 123, - &BrokerCallerRole::AdminUid { uid: 1000 }, - None, - ) - .expect("audit context"); - let error = dispatch_request_with_request_fds( - request, - 1000, - 100, - BrokerCallerRole::AdminUid { uid: 1000 }, - &context, - &config, - &audit_log, - None, - Vec::new(), - ) - .expect_err("admin peer must be denied"); - assert!(matches!(error, BrokerError::AuditRequiresAdmin)); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn unregistered_runner_capability_is_refused() { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-audit-unregistered-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let audit_log = - AuditLog::open(&config.audit_dir, config.d2bd_gid, true, 30).expect("audit log"); - let request = BrokerRequest::CutoverAudit(CutoverAuditRequest { - operation_id: BundleOpId::new("op-unregistered-runner"), - phase: 3, - transition: CutoverAuditTransition::HoldRequested, - request_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)) - .expect("request digest"), - reason_digest: None, - }); - let role = BrokerCallerRole::CutoverRunner { - operation_id: BundleOpId::new("op-unregistered-runner"), - capability_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"b".repeat(64)) - .expect("capability digest"), - }; - let context = DispatchAuditContext::from_request_with_join(&request, 123, &role, None) - .expect("audit context"); - let error = dispatch_request_with_request_fds( - request, - 0, - 0, - role, - &context, - &config, - &audit_log, - None, - Vec::new(), - ) - .expect_err("unregistered capability must be refused"); - assert!(matches!( - error, - BrokerError::PeerCredentialRefused { - operation: "CutoverAudit" - } - )); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn runner_peer_allowlist_excludes_full_daemon_operations() { - assert!(request_allows_cutover_runner(&request())); - assert!(!request_allows_cutover_runner( - &BrokerRequest::ValidateBundle - )); - } - - #[test] - fn adapted_runner_effect_rejects_capability_digest_mismatch() { - let operation_id = BundleOpId::new("op-effect-runtime"); - let request = BrokerRequest::CutoverEffect(CutoverEffectRequest { - operation_id: operation_id.clone(), - authority: CutoverEffectAuthority::Cutover, - phase: 5, - effect_id: BundleOpId::new("effect-runtime"), - effect: CutoverEffectKind::ClosureActivation, - replay_class: CutoverReplayClass::ReopenByJournaledIdentity, - request_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)) - .expect("request digest"), - capability_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"c".repeat(64)) - .expect("mismatched capability digest"), - identity: None, - handoff: None, - payload: None, - }); - let role = runner_role(operation_id.clone()); - let context = DispatchAuditContext::from_request_with_join(&request, 123, &role, None) - .expect("audit context"); - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-effect-deny-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let audit_log = - AuditLog::open(&config.audit_dir, config.d2bd_gid, true, 30).expect("audit log"); - let capability_digest = match &role { - BrokerCallerRole::CutoverRunner { - capability_digest, .. - } => capability_digest, - _ => unreachable!(), - }; - register_cutover_capability(&config, &operation_id, capability_digest, u64::MAX) - .expect("register test capability"); - let error = dispatch_request_with_request_fds( - request, - 0, - 0, - role, - &context, - &config, - &audit_log, - None, - Vec::new(), - ) - .expect_err("mismatched capability must be refused"); - assert!(matches!( - error, - BrokerError::PeerCredentialRefused { - operation: "CutoverEffect" - } - )); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn nonclosure_effect_reports_its_later_owner_precisely() { - let operation_id = BundleOpId::new("op-effect-owner"); - let request = BrokerRequest::CutoverEffect(CutoverEffectRequest { - operation_id: operation_id.clone(), - authority: CutoverEffectAuthority::Cutover, - phase: 4, - effect_id: BundleOpId::new("effect-internal-marker"), - effect: CutoverEffectKind::CutoverBroker, - replay_class: CutoverReplayClass::QuarantineOnly, - request_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)) - .expect("request digest"), - capability_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"b".repeat(64)) - .expect("capability digest"), - identity: None, - handoff: None, - payload: None, - }); - let role = runner_role(operation_id.clone()); - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-effect-owner-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let capability_digest = match &role { - BrokerCallerRole::CutoverRunner { - capability_digest, .. - } => capability_digest, - _ => unreachable!(), - }; - register_cutover_capability(&config, &operation_id, capability_digest, u64::MAX) - .expect("register test capability"); - let audit_log = - AuditLog::open(&config.audit_dir, config.d2bd_gid, true, 30).expect("audit log"); - let context = DispatchAuditContext::from_request_with_join(&request, 123, &role, None) - .expect("audit context"); - let error = dispatch_request_with_request_fds( - request, - 0, - 0, - role, - &context, - &config, - &audit_log, - None, - Vec::new(), - ) - .expect_err("later-owner effect must be blocked"); - assert!(matches!( - error, - BrokerError::Unimplemented { - operation: "CutoverEffect.CutoverBroker", - target_wave: "ADR046-reset-003" - } - )); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn host_drain_closes_the_broker_to_non_runner_operations() { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-window-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let operation_id = BundleOpId::new("op-cutover-window"); - let digest = - CanonicalAuditDigest::parse("sha256:".to_owned() + &"b".repeat(64)).expect("digest"); - register_cutover_capability(&config, &operation_id, &digest, u64::MAX) - .expect("register capability"); - assert!(!cutover_window_active(&config).expect("window status")); - mark_cutover_capability_drained(&config, &operation_id).expect("mark drained"); - assert!(cutover_window_active(&config).expect("window status")); - assert!(request_allows_cutover_runner(&BrokerRequest::CutoverAudit( - CutoverAuditRequest { - operation_id, - phase: 3, - transition: CutoverAuditTransition::PhaseCompleted, - request_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)) - .expect("request digest"), - reason_digest: None, - } - ))); - assert!(!request_allows_cutover_runner( - &BrokerRequest::ValidateBundle - )); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn failed_cutover_launch_releases_its_capability_binding() { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-launch-release-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let operation_id = BundleOpId::new("op-cutover-launch-release"); - let digest = - CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)).expect("digest"); - - register_cutover_capability(&config, &operation_id, &digest, u64::MAX) - .expect("register capability"); - remove_cutover_capability(&config, &operation_id).expect("remove capability"); - assert!( - !cutover_capability_matches(&config, &operation_id, &digest) - .expect("capability lookup") - ); - register_cutover_capability(&config, &operation_id, &digest, u64::MAX) - .expect("retry registration"); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn exited_cutover_runner_capability_can_be_replaced() { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-capability-retry-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let operation_id = BundleOpId::new("op-cutover-capability-retry"); - let first_digest = - CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)).expect("digest"); - let second_digest = - CanonicalAuditDigest::parse("sha256:".to_owned() + &"b".repeat(64)).expect("digest"); - - register_cutover_capability(&config, &operation_id, &first_digest, u64::MAX) - .expect("register capability"); - bind_cutover_capability_runner(&config, &operation_id, i32::MAX, 1) - .expect("bind exited runner identity"); - register_cutover_capability(&config, &operation_id, &second_digest, u64::MAX) - .expect("replace exited runner capability"); - assert!( - cutover_capability_matches(&config, &operation_id, &second_digest) - .expect("capability lookup") - ); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn drained_dead_cutover_runner_allows_only_a_narrow_relaunch() { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(".scratch") - .join(format!("cutover-relaunch-window-{}", std::process::id())); - fs::create_dir_all(&root).expect("test root"); - let config = config(&root); - let operation_id = BundleOpId::new("op-cutover-relaunch-window"); - let digest = - CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)).expect("digest"); - register_cutover_capability(&config, &operation_id, &digest, u64::MAX) - .expect("register capability"); - bind_cutover_capability_runner(&config, &operation_id, i32::MAX, 1) - .expect("bind exited runner identity"); - mark_cutover_capability_drained(&config, &operation_id).expect("mark drained"); - - let launch = d2b_contracts_broker::broker_wire::BrokerRequest::LaunchCutoverRunner( - d2b_contracts_broker::broker_wire::LaunchCutoverRunnerRequest { - operation_id: operation_id.clone(), - bootstrap_fd_index: 0, - capability_digest: digest, - expires_at_ms: u64::MAX, - }, - ); - assert!(cutover_relaunch_allowed(&config, &launch).expect("relaunch decision")); - assert!( - !cutover_relaunch_allowed( - &config, - &d2b_contracts_broker::broker_wire::BrokerRequest::ValidateBundle - ) - .expect("non-launch decision") - ); - let _ = fs::remove_dir_all(root); - } -} - #[derive(Debug, Clone)] pub enum BrokerMode { - Serve(ServerConfig), + Host(ServerConfig), + Guest(ServerConfig), #[cfg(feature = "layer1-bootstrap")] ProbeHello { socket_path: PathBuf, @@ -1007,9 +321,9 @@ enum BrokerError { PeerCredentialRefused { operation: &'static str, }, - #[cfg_attr(feature = "layer1-bootstrap", allow(dead_code))] - GuestControlSignRefused { - reason: &'static str, + ProfileOperationRefused { + profile: BrokerProfile, + operation: &'static str, }, /// swtpm-dir first-run hardening (issue #64) refused to proceed. /// Carries the path-free [`OperationFields::PrepareSwtpmDir`] audit @@ -1040,193 +354,240 @@ where I: IntoIterator, { let mut args = args.into_iter(); - let subcommand = args.next().unwrap_or_else(|| "serve".to_owned()); + let subcommand = args + .next() + .ok_or_else(|| RunError::Usage("usage: d2b-broker [host|guest] [options]".to_owned()))?; + #[cfg(feature = "layer1-bootstrap")] match subcommand.as_str() { - "serve" => { - // --socket-path is optional. Resolution order: - // 1. --socket-path flag (explicit override) - // 2. D2B_BROKER_SOCKET_PATH env var - // 3. DEFAULT_SOCKET_PATH constant ("/run/d2b/priv.sock") - // Under SD_LISTEN_FDS=1 (socket activation) the resolved path is - // informational only; the broker adopts fd 3 from systemd and - // MUST NOT bind, fchmod, or fchown the socket path. - let mut socket_path_override: Option = None; - let mut audit_dir = PathBuf::from(DEFAULT_AUDIT_DIR); - let mut audit_retention_days = DEFAULT_AUDIT_RETENTION_DAYS; - let mut bundle_path = PathBuf::from(DEFAULT_BUNDLE_PATH); - let mut realm_controllers_path = PathBuf::from(DEFAULT_REALM_CONTROLLERS_PATH); - let mut realm_identity_path = PathBuf::from(DEFAULT_REALM_IDENTITY_PATH); - let mut state_dir = PathBuf::from(DEFAULT_STATE_DIR); - let mut activation_helper_path = PathBuf::from(DEFAULT_ACTIVATION_HELPER_PATH); - let mut store_sync_export_dir = - PathBuf::from(crate::ops::store_sync_export::DEFAULT_STORE_SYNC_EXPORT_DIR); - let mut d2bd_uid = None; - let mut d2bd_gid = None; - let mut test_mode = false; - let rest: Vec = args.collect(); - let mut index = 0; - while index < rest.len() { - match rest[index].as_str() { - "--socket-path" => { - index += 1; - socket_path_override = - Some(PathBuf::from(expect_arg(&rest, index, "--socket-path")?)); - } - "--audit-dir" => { - index += 1; - audit_dir = PathBuf::from(expect_arg(&rest, index, "--audit-dir")?); - } - "--audit-retention-days" => { - index += 1; - audit_retention_days = expect_arg(&rest, index, "--audit-retention-days")? - .parse() - .map_err(|_| { - RunError::Usage( - "invalid --audit-retention-days (expected a non-negative integer; 0 disables pruning)" - .to_owned(), - ) - })?; - } - "--bundle-path" => { - // Broker reads the bundle manifest from this - // server-configured path so the daemon never - // names a bundle path on the wire. - index += 1; - bundle_path = PathBuf::from(expect_arg(&rest, index, "--bundle-path")?); - } - "--realm-controllers-path" => { - index += 1; - realm_controllers_path = - PathBuf::from(expect_arg(&rest, index, "--realm-controllers-path")?); - } - "--realm-identity-path" => { - index += 1; - realm_identity_path = - PathBuf::from(expect_arg(&rest, index, "--realm-identity-path")?); - } - "--state-dir" => { - index += 1; - state_dir = PathBuf::from(expect_arg(&rest, index, "--state-dir")?); - } - "--activation-helper-path" => { - index += 1; - activation_helper_path = - PathBuf::from(expect_arg(&rest, index, "--activation-helper-path")?); - } - "--store-sync-export-dir" => { - index += 1; - store_sync_export_dir = - PathBuf::from(expect_arg(&rest, index, "--store-sync-export-dir")?); - } - "--d2bd-uid" => { - index += 1; - d2bd_uid = Some( - expect_arg(&rest, index, "--d2bd-uid")? - .parse() - .map_err(|_| RunError::Usage("invalid --d2bd-uid".to_owned()))?, - ); - } - "--d2bd-gid" => { - index += 1; - d2bd_gid = Some( - expect_arg(&rest, index, "--d2bd-gid")? - .parse() - .map_err(|_| RunError::Usage("invalid --d2bd-gid".to_owned()))?, - ); - } - "--test-mode" => test_mode = true, - other => { - return Err(RunError::Usage(format!("unknown serve flag: {other}"))); - } - } - index += 1; - } - - // Resolve socket path: flag > env var > built-in default. - let socket_path = socket_path_override - .or_else(|| env::var("D2B_BROKER_SOCKET_PATH").ok().map(PathBuf::from)) - .unwrap_or_else(|| PathBuf::from(DEFAULT_SOCKET_PATH)); - - let fallback_uid = if test_mode { - nix::unistd::Uid::current().as_raw() - } else { - env::var("D2BD_UID") - .ok() - .and_then(|value| value.parse().ok()) - .ok_or_else(|| { - RunError::Usage( - "missing d2bd uid: pass --d2bd-uid or set D2BD_UID".to_owned(), - ) - })? - }; - let fallback_gid = if test_mode { - nix::unistd::Gid::current().as_raw() - } else { - env::var("D2BD_GID") - .ok() - .and_then(|value| value.parse().ok()) - .ok_or_else(|| { - RunError::Usage( - "missing d2bd gid: pass --d2bd-gid or set D2BD_GID".to_owned(), - ) - })? - }; - - Ok(BrokerMode::Serve(ServerConfig { - socket_path, - audit_dir, - audit_retention_days, - bundle_path, - realm_controllers_path, - realm_identity_path, - state_dir, - activation_helper_path, - cutover_runner_path: env::var_os("D2B_CUTOVER_RUNNER_PATH") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(DEFAULT_CUTOVER_RUNNER_PATH)), - d2bd_uid: d2bd_uid.unwrap_or(fallback_uid), - d2bd_gid: d2bd_gid.unwrap_or(fallback_gid), - store_sync_export_dir, - test_mode, - })) - } - #[cfg(feature = "layer1-bootstrap")] "probe-hello" => { let (socket_path, test_uid) = parse_probe_flags(args.collect())?; - Ok(BrokerMode::ProbeHello { + return Ok(BrokerMode::ProbeHello { socket_path, test_uid, - }) + }); } - #[cfg(feature = "layer1-bootstrap")] "probe-stub" => { let rest: Vec = args.collect(); let (socket_path, test_uid, operation) = parse_stub_flags(&rest)?; - Ok(BrokerMode::ProbeStub { + return Ok(BrokerMode::ProbeStub { socket_path, test_uid, operation, - }) + }); } - #[cfg(feature = "layer1-bootstrap")] "probe-export-audit" => { let rest: Vec = args.collect(); let (socket_path, test_uid, caller_role) = parse_export_flags(&rest)?; - Ok(BrokerMode::ProbeExportAudit { + return Ok(BrokerMode::ProbeExportAudit { socket_path, test_uid, caller_role, - }) + }); } - _ => Err(RunError::Usage( - "usage: d2b-priv-broker [serve|probe-hello|probe-stub|probe-export-audit]".to_owned(), - )), + _ => {} } + let profile = match subcommand.as_str() { + "host" => BrokerProfile::Host, + "guest" => BrokerProfile::Guest, + other => { + return Err(RunError::Usage(format!( + "usage: d2b-broker [host|guest] [options] (unknown profile: {other})" + ))); + } + }; + // --socket-path is optional. Resolution order: + // 1. --socket-path flag (explicit override) + // 2. D2B_BROKER_SOCKET_PATH env var + // 3. DEFAULT_SOCKET_PATH constant ("/run/d2b/priv.sock") + // Under SD_LISTEN_FDS=1 (socket activation) the resolved path is + // informational only; the broker adopts fd 3 from systemd and + // MUST NOT bind, fchmod, or fchown the socket path. + let mut socket_path_override: Option = None; + let mut audit_dir = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_AUDIT_DIR, + BrokerProfile::Guest => DEFAULT_GUEST_AUDIT_DIR, + }); + let mut audit_retention_days = DEFAULT_AUDIT_RETENTION_DAYS; + let mut bundle_path = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_BUNDLE_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_BUNDLE_PATH, + }); + let mut realm_controllers_path = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_REALM_CONTROLLERS_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_REALM_CONTROLLERS_PATH, + }); + let mut realm_identity_path = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_REALM_IDENTITY_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_REALM_IDENTITY_PATH, + }); + let mut state_dir = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_STATE_DIR, + BrokerProfile::Guest => DEFAULT_GUEST_STATE_DIR, + }); + let mut activation_helper_path = PathBuf::from(DEFAULT_ACTIVATION_HELPER_PATH); + let mut store_sync_export_dir = match profile { + BrokerProfile::Host => { + PathBuf::from(crate::ops::store_sync_export::DEFAULT_STORE_SYNC_EXPORT_DIR) + } + BrokerProfile::Guest => PathBuf::from("/var/lib/d2b/guest-audit/store-sync"), + }; + let mut authority_id = profile.as_str().to_owned(); + let mut d2bd_uid = None; + let mut d2bd_gid = None; + let mut test_mode = false; + let rest: Vec = args.collect(); + let mut index = 0; + while index < rest.len() { + match rest[index].as_str() { + "--socket-path" => { + index += 1; + socket_path_override = + Some(PathBuf::from(expect_arg(&rest, index, "--socket-path")?)); + } + "--audit-dir" => { + index += 1; + audit_dir = PathBuf::from(expect_arg(&rest, index, "--audit-dir")?); + } + "--audit-retention-days" => { + index += 1; + audit_retention_days = expect_arg(&rest, index, "--audit-retention-days")? + .parse() + .map_err(|_| { + RunError::Usage( + "invalid --audit-retention-days (expected a non-negative integer; 0 disables pruning)" + .to_owned(), + ) + })?; + } + "--bundle-path" => { + // Broker reads the bundle manifest from this + // server-configured path so the daemon never + // names a bundle path on the wire. + index += 1; + bundle_path = PathBuf::from(expect_arg(&rest, index, "--bundle-path")?); + } + "--realm-controllers-path" => { + index += 1; + realm_controllers_path = + PathBuf::from(expect_arg(&rest, index, "--realm-controllers-path")?); + } + "--realm-identity-path" => { + index += 1; + realm_identity_path = + PathBuf::from(expect_arg(&rest, index, "--realm-identity-path")?); + } + "--state-dir" => { + index += 1; + state_dir = PathBuf::from(expect_arg(&rest, index, "--state-dir")?); + } + "--activation-helper-path" => { + index += 1; + activation_helper_path = + PathBuf::from(expect_arg(&rest, index, "--activation-helper-path")?); + } + "--store-sync-export-dir" => { + index += 1; + store_sync_export_dir = + PathBuf::from(expect_arg(&rest, index, "--store-sync-export-dir")?); + } + "--authority-id" => { + index += 1; + authority_id = expect_arg(&rest, index, "--authority-id")?.to_owned(); + if authority_id.is_empty() { + return Err(RunError::Usage( + "--authority-id must not be empty".to_owned(), + )); + } + } + "--d2bd-uid" => { + index += 1; + d2bd_uid = Some( + expect_arg(&rest, index, "--d2bd-uid")? + .parse() + .map_err(|_| RunError::Usage("invalid --d2bd-uid".to_owned()))?, + ); + } + "--d2bd-gid" => { + index += 1; + d2bd_gid = Some( + expect_arg(&rest, index, "--d2bd-gid")? + .parse() + .map_err(|_| RunError::Usage("invalid --d2bd-gid".to_owned()))?, + ); + } + "--test-mode" => test_mode = true, + other => { + return Err(RunError::Usage(format!( + "unknown {} flag: {other}", + profile.as_str() + ))); + } + } + index += 1; + } + + // Resolve socket path: flag > env var > built-in default. + let socket_path = socket_path_override + .or_else(|| { + let variable = match profile { + BrokerProfile::Host => "D2B_BROKER_SOCKET_PATH", + BrokerProfile::Guest => "D2B_GUEST_BROKER_SOCKET_PATH", + }; + env::var(variable).ok().map(PathBuf::from) + }) + .unwrap_or_else(|| { + PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_SOCKET_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_SOCKET_PATH, + }) + }); + + let fallback_uid = if test_mode { + nix::unistd::Uid::current().as_raw() + } else { + env::var("D2BD_UID") + .ok() + .and_then(|value| value.parse().ok()) + .ok_or_else(|| { + RunError::Usage("missing d2bd uid: pass --d2bd-uid or set D2BD_UID".to_owned()) + })? + }; + let fallback_gid = if test_mode { + nix::unistd::Gid::current().as_raw() + } else { + env::var("D2BD_GID") + .ok() + .and_then(|value| value.parse().ok()) + .ok_or_else(|| { + RunError::Usage("missing d2bd gid: pass --d2bd-gid or set D2BD_GID".to_owned()) + })? + }; + + let config = ServerConfig { + profile, + authority_id, + socket_path, + audit_dir, + audit_retention_days, + bundle_path, + realm_controllers_path, + realm_identity_path, + state_dir, + activation_helper_path, + d2bd_uid: d2bd_uid.unwrap_or(fallback_uid), + d2bd_gid: d2bd_gid.unwrap_or(fallback_gid), + store_sync_export_dir, + test_mode, + }; + Ok(match profile { + BrokerProfile::Host => BrokerMode::Host(config), + BrokerProfile::Guest => BrokerMode::Guest(config), + }) } pub fn run(command: BrokerMode) -> Result<(), RunError> { match command { - BrokerMode::Serve(config) => run_server(config), + BrokerMode::Host(config) | BrokerMode::Guest(config) => run_server(config), #[cfg(feature = "layer1-bootstrap")] BrokerMode::ProbeHello { socket_path, @@ -1623,6 +984,19 @@ fn try_load_resolver_with_policy( } } +/// Bind a kernel-authenticated peer to this broker instance before any wire +/// bytes are decoded. Test mode keeps the existing simulated envelope UID +/// support, but still requires the actual local test process credentials. +fn peer_matches_instance(config: &ServerConfig, peer_uid: u32, peer_gid: u32) -> bool { + if config.test_mode { + return (peer_uid == nix::unistd::Uid::current().as_raw() + && peer_gid == nix::unistd::Gid::current().as_raw()) + || peer_uid == 0; + } + (peer_uid == config.d2bd_uid && peer_gid == config.d2bd_gid) + || (config.profile == BrokerProfile::Host && peer_uid == 0) +} + fn handle_connection( fd: OwnedFd, config: &ServerConfig, @@ -1633,6 +1007,23 @@ fn handle_connection( ipc_rate_limiter: &Arc>, ) -> io::Result<()> { let (peer_uid, peer_gid, peer_pid) = peer_credentials(fd.as_raw_fd())?; + if !peer_matches_instance(config, peer_uid, peer_gid) { + let _ = write_refusal_audit_bounded( + audit_log, + AuditWriteClass::Unprivileged, + "PeerAuthentication", + peer_uid, + peer_gid, + "peer-refused-before-decode", + "broker-instance", + "closed", + ); + // Do not decode or synchronously drain an untrusted frame. Closing + // the accepted socket is the fail-closed response and avoids letting + // an unauthenticated peer hold a broker worker while it trickles + // request bytes. + return Ok(()); + } #[cfg(not(feature = "layer1-bootstrap"))] let (envelope, request_fds) = match recv_json_frame_with_fds::(fd.as_raw_fd())? { @@ -1645,12 +1036,6 @@ fn handle_connection( None => return Ok(()), }; let request = envelope.request; - #[cfg(not(feature = "layer1-bootstrap"))] - let direct_cutover_runner = peer_uid == 0 - && envelope.caller_role.is_cutover_runner() - && request_allows_cutover_runner(&request); - #[cfg(feature = "layer1-bootstrap")] - let direct_cutover_runner = false; let effective_uid = if config.test_mode { envelope.test_peer_uid.unwrap_or(peer_uid) } else { @@ -1659,46 +1044,31 @@ fn handle_connection( let operation = request.op_name(); let opaque_target_id = request.opaque_target_id(); #[cfg(not(feature = "layer1-bootstrap"))] - let cutover_window = match cutover_window_active(config) { - Ok(active) => active, - Err(error) => { - let _ = write_refusal_audit_bounded( - audit_log, - AuditWriteClass::Privileged, - operation, - effective_uid, - peer_gid, - "cutover-window-registry-failed", - opaque_target_id, - "closed", - ); - send_json_frame(fd.as_raw_fd(), &error.into_response())?; - return Ok(()); - } - }; - #[cfg(feature = "layer1-bootstrap")] - let cutover_window = false; - #[cfg(not(feature = "layer1-bootstrap"))] - let cutover_relaunch = cutover_window - && effective_uid == config.d2bd_uid - && cutover_relaunch_allowed(config, &request).unwrap_or(false); - #[cfg(feature = "layer1-bootstrap")] - let cutover_relaunch = false; - if cutover_window && !direct_cutover_runner && !cutover_relaunch { - let error = BrokerError::PeerCredentialRefused { operation }; + if !config.profile.allows_request(&request) { + let error = BrokerError::ProfileOperationRefused { + profile: config.profile, + operation, + }; let _ = write_refusal_audit_bounded( audit_log, AuditWriteClass::Privileged, operation, - effective_uid, + peer_uid, peer_gid, - "cutover-window-closed-allowlist", + "profile-operation-denied", opaque_target_id, "closed", ); send_json_frame(fd.as_raw_fd(), &error.into_response())?; return Ok(()); } + #[cfg(not(feature = "layer1-bootstrap"))] + if config.profile == BrokerProfile::Guest { + if let Err(error) = validate_guest_process_binding(&request) { + send_json_frame(fd.as_raw_fd(), &error.into_response())?; + return Ok(()); + } + } let (rate_role, rate_operation) = if effective_uid == config.d2bd_uid { (envelope.caller_role.for_display(), operation) } else { @@ -1735,7 +1105,7 @@ fn handle_connection( } return Ok(()); } - if effective_uid != config.d2bd_uid && !direct_cutover_runner { + if effective_uid != config.d2bd_uid { if let Err(error) = write_refusal_audit_bounded( audit_log, AuditWriteClass::Unprivileged, @@ -2316,11 +1686,6 @@ impl DispatchAuditContext { } fn request_fields_value(request: &BrokerRequest) -> Result { - // GuestControlSign carries auth secret material (nonces, token-derived - // tag inputs); emit only redacted lengths/presence, never the values. - // This branch is real-wire-only: under the `layer1-bootstrap` feature - // `BrokerRequest` aliases to the bootstrap `BootstrapCall`, which has no - // GuestControlSign variant, so gate it out of the bootstrap build. #[cfg(not(feature = "layer1-bootstrap"))] if let BrokerRequest::QemuMediaEnroll(req) = request { return Ok(serde_json::json!({ @@ -2397,19 +1762,6 @@ fn request_fields_value(request: &BrokerRequest) -> Result { "tracingSpanIdPresent": req.tracing_span_id.is_some(), })); } - #[cfg(not(feature = "layer1-bootstrap"))] - if let BrokerRequest::GuestControlSign(req) = request { - return Ok(serde_json::json!({ - "vmId": req.vm_id.as_str(), - "role": format!("{:?}", req.role), - "purpose": format!("{:?}", req.purpose), - "hostNonceLen": req.host_nonce.len(), - "guestNonceLen": req.guest_nonce.len(), - "guestBootIdPresent": !req.guest_boot_id.as_str().is_empty(), - "peerCidPresent": req.peer_cid.is_some(), - "capabilitiesHashPresent": req.capabilities_hash.is_some(), - })); - } let mut value = serde_json::to_value(request) .map_err(|err| BrokerError::Protocol(format!("serialize request fields: {err}")))?; match &mut value { @@ -2448,7 +1800,7 @@ fn dispatch_request( "ok", ) .map_err(|err| BrokerError::Protocol(err.to_string()))?; - Ok(hello_ok_response()) + Ok(hello_ok_response(_config.profile)) } BrokerRequest::ValidateBundle { path } => { handle_validate_bundle(&path, caller_uid, caller_gid, audit_log) @@ -2577,514 +1929,46 @@ fn dispatch_request( operation: "SetupMountNamespace", target_wave: "W7", }), - BrokerRequest::SpawnRunner { .. } => Err(BrokerError::Unimplemented { - operation: "SpawnRunner", - target_wave: "W4-fu", - }), - BrokerRequest::UpdateHostsFile { .. } => Err(BrokerError::Unimplemented { - operation: "UpdateHostsFile", - target_wave: "W3", - }), - BrokerRequest::UsbipBind { .. } => Err(BrokerError::UnknownOperation { - operation: "UsbipBind", - }), - BrokerRequest::UsbipBindFirewallRule { .. } => Err(BrokerError::Unimplemented { - operation: "UsbipBindFirewallRule", - target_wave: "W3", - }), - BrokerRequest::UsbipExplicitBind { .. } => Err(BrokerError::Unimplemented { - operation: "UsbipExplicitBind", - target_wave: "W5", - }), - BrokerRequest::UsbipExplicitFirewallRule { .. } => Err(BrokerError::Unimplemented { - operation: "UsbipExplicitFirewallRule", - target_wave: "W5", - }), - BrokerRequest::UsbipProxyReconcile { .. } => Err(BrokerError::UnknownOperation { - operation: "UsbipProxyReconcile", - }), - BrokerRequest::UsbipUnbind { .. } => Err(BrokerError::UnknownOperation { - operation: "UsbipUnbind", - }), - } -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn request_accepts_fd(request: &BrokerRequest) -> bool { - matches!( - request, - BrokerRequest::OpenPeerPidfdFromAcceptedSocket(_) | BrokerRequest::LaunchCutoverRunner(_) - ) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn request_allows_cutover_runner(request: &BrokerRequest) -> bool { - matches!( - request, - BrokerRequest::CutoverAudit(_) | BrokerRequest::CutoverEffect(_) - ) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_effect_operation_name( - effect: d2b_contracts_broker::broker_wire::CutoverEffectKind, -) -> &'static str { - match effect { - d2b_contracts_broker::broker_wire::CutoverEffectKind::ApplyAdmission => { - "CutoverEffect.ApplyAdmission" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::CutoverDisposition => { - "CutoverEffect.CutoverDisposition" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::ResourceStoreCreate => { - "CutoverEffect.ResourceStoreCreate" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::ProviderInstall => { - "CutoverEffect.ProviderInstall" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::ZoneActivation => { - "CutoverEffect.ZoneActivation" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::GuestActivation => { - "CutoverEffect.GuestActivation" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::Verification => "CutoverEffect.Verification", - d2b_contracts_broker::broker_wire::CutoverEffectKind::CutoverFinalization => { - "CutoverEffect.CutoverFinalization" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::ScopedZoneReset => { - "CutoverEffect.ScopedZoneReset" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::ScopedProviderReset => { - "CutoverEffect.ScopedProviderReset" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::ScopedGuestReset => { - "CutoverEffect.ScopedGuestReset" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::DestroyDurableVolume => { - "CutoverEffect.DestroyDurableVolume" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::PreserveSource => { - "CutoverEffect.PreserveSource" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::QuarantineDestination => { - "CutoverEffect.QuarantineDestination" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::CutoverBroker => { - "CutoverEffect.CutoverBroker" - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::HostDrain - | d2b_contracts_broker::broker_wire::CutoverEffectKind::ClosureActivation => "CutoverEffect", - } -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_effect_owner(effect: d2b_contracts_broker::broker_wire::CutoverEffectKind) -> &'static str { - use d2b_contracts_broker::broker_wire::CutoverEffectKind; - match effect { - CutoverEffectKind::ApplyAdmission => "ADR046-reset-001", - CutoverEffectKind::CutoverDisposition - | CutoverEffectKind::PreserveSource - | CutoverEffectKind::QuarantineDestination => "ADR046-reset-003/004", - CutoverEffectKind::ResourceStoreCreate | CutoverEffectKind::ProviderInstall => { - "ADR046-reset-005" - } - CutoverEffectKind::ZoneActivation | CutoverEffectKind::GuestActivation => { - "ADR046-reset-006" - } - CutoverEffectKind::Verification => "ADR046-reset-007", - CutoverEffectKind::CutoverFinalization => "ADR046-reset-008", - CutoverEffectKind::ScopedZoneReset - | CutoverEffectKind::ScopedProviderReset - | CutoverEffectKind::ScopedGuestReset - | CutoverEffectKind::DestroyDurableVolume => "ADR046-reset-010", - CutoverEffectKind::CutoverBroker => "ADR046-reset-003", - CutoverEffectKind::HostDrain | CutoverEffectKind::ClosureActivation => "U4", - } -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn execute_cutover_verification( - request: &d2b_contracts_broker::broker_wire::CutoverVerificationRequest, - resolver: &BundleResolver, -) -> d2b_contracts_broker::broker_wire::CutoverVerificationResponse { - use d2b_contracts_broker::broker_wire::{CutoverVerificationResponse, CutoverZoneVerification}; - use d2b_contracts::types::BundleOpId; - let configured_zone_ids = resolver - .bundle - .artifact_hashes - .as_ref() - .into_iter() - .flat_map(|artifacts| artifacts.keys()) - .filter_map(|key| { - key.strip_prefix("zones/") - .and_then(|value| value.strip_suffix("/storage.json")) - .filter(|value| !value.is_empty() && !value.contains('/') && !value.contains('\\')) - .map(BundleOpId::new) - }) - .collect::>(); - let expected_zone_ids = request - .expected_zone_ids - .iter() - .cloned() - .collect::>(); - let binding_matches = !configured_zone_ids.is_empty() - && configured_zone_ids.len() == expected_zone_ids.len() - && configured_zone_ids - .iter() - .all(|zone_id| expected_zone_ids.contains(zone_id)); - let zones: Vec<_> = configured_zone_ids - .iter() - .cloned() - .map(|zone_id| CutoverZoneVerification { - healthy: binding_matches && live_zone_store_view_is_healthy(resolver, &zone_id), - zone_id, - }) - .collect(); - let all_zones_healthy = zones.iter().all(|zone| zone.healthy); - CutoverVerificationResponse { - zones, - sources_preserved: binding_matches && all_zones_healthy, - identity_digests_match: all_zones_healthy - && resolver.installed_generation_identity().is_some(), - candidate_current: all_zones_healthy && resolver.bundle.bundle_hash.is_some(), - } -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn execute_cutover_admission( - request: &d2b_contracts_broker::broker_wire::CutoverAdmissionRequest, - authority: d2b_contracts_broker::broker_wire::CutoverEffectAuthority, - resolver: &BundleResolver, - activation_helper_path: &std::path::Path, -) -> d2b_contracts_broker::broker_wire::CutoverAdmissionResponse { - use d2b_contracts::types::BundleOpId; - let zone_ids = resolver - .bundle - .artifact_hashes - .as_ref() - .into_iter() - .flat_map(|artifacts| artifacts.keys()) - .filter_map(|key| { - key.strip_prefix("zones/") - .and_then(|value| value.strip_suffix("/storage.json")) - .filter(|value| !value.is_empty() && !value.contains('/') && !value.contains('\\')) - .map(BundleOpId::new) - }) - .collect::>(); - let store_views_healthy = !zone_ids.is_empty() - && zone_ids - .iter() - .all(|zone_id| live_zone_store_view_is_healthy(resolver, zone_id)); - let candidate_current = resolver.bundle.bundle_hash.is_some() && store_views_healthy; - let markers_valid = store_views_healthy; - let ownership_valid = store_views_healthy && resolver.installed_generation_identity().is_some(); - let artifacts_valid = if !artifact_binding_shape_is_valid(request, authority) { - false - } else if let (Some(candidate), Some(source)) = ( - request.system_artifact_id.as_ref(), - request.source_system_artifact_id.as_ref(), - ) { - crate::ops::host_generation_handoff::validate_artifact_with_helper( - activation_helper_path, - candidate, - ) - .unwrap_or(false) - && crate::ops::host_generation_handoff::validate_artifact_with_helper( - activation_helper_path, - source, - ) - .unwrap_or(false) - } else { - true - }; - d2b_contracts_broker::broker_wire::CutoverAdmissionResponse { - candidate_current, - markers_valid, - ownership_valid, - predicates_hold: candidate_current && markers_valid && ownership_valid && artifacts_valid, - } -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn artifact_binding_shape_is_valid( - request: &d2b_contracts_broker::broker_wire::CutoverAdmissionRequest, - authority: d2b_contracts_broker::broker_wire::CutoverEffectAuthority, -) -> bool { - match authority { - d2b_contracts_broker::broker_wire::CutoverEffectAuthority::Cutover => { - request.system_artifact_id.is_some() && request.source_system_artifact_id.is_some() - } - _ => request.system_artifact_id.is_none() && request.source_system_artifact_id.is_none(), - } -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn live_zone_store_view_is_healthy( - resolver: &BundleResolver, - zone_id: &d2b_contracts::types::BundleOpId, -) -> bool { - let Some(intent) = resolver.find_store_view_intent(zone_id.as_str()) else { - return false; - }; - matches!( - crate::ops::store_verify::run_store_verify_read_only(intent, false).status, - d2b_contracts_broker::broker_wire::StoreVerifyStatus::Ok - ) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn execute_cutover_effect_payload( - effect: d2b_contracts_broker::broker_wire::CutoverEffectKind, - payload: Option, - resolver: Option<&Arc>, - backend: &B, - state_dir: &Path, - operation_id: &d2b_contracts::types::BundleOpId, -) -> Result< - ( - d2b_contracts_broker::broker_wire::CutoverEffectOutcome, - Option, - ), - BrokerError, -> { - use d2b_contracts_broker::broker_wire::{ - CutoverEffectKind, CutoverEffectOutcome, CutoverEffectPayload, StorageReconcileStatus, - StoreVerifyStatus, - }; - let resolver_ref = || require_resolver_ref(resolver.map(std::sync::Arc::as_ref)); - match effect { - CutoverEffectKind::CutoverDisposition - | CutoverEffectKind::PreserveSource - | CutoverEffectKind::ScopedZoneReset - | CutoverEffectKind::ScopedProviderReset - | CutoverEffectKind::ScopedGuestReset => { - let CutoverEffectPayload::Storage(request) = payload - .ok_or_else(|| BrokerError::Protocol("storage payload required".to_owned()))? - else { - return Err(BrokerError::Protocol( - "storage effect payload kind mismatch".to_owned(), - )); - }; - if effect != CutoverEffectKind::PreserveSource && !request.apply { - return Err(BrokerError::Protocol( - "mutating storage effect requires apply=true".to_owned(), - )); - } - if effect == CutoverEffectKind::PreserveSource && request.apply { - return Err(BrokerError::Protocol( - "preserve effect requires apply=false".to_owned(), - )); - } - let resolver = resolver_ref()?; - let response = crate::ops::storage_contract::reconcile_storage_scope( - resolver, - &request.storage_ref, - if effect == CutoverEffectKind::PreserveSource { - false - } else { - request.apply - }, - ) - .map_err(|err| match err { - crate::ops::storage_contract::StorageContractError::UnknownStorage(id) => { - BrokerError::BundleIntentMissing { - kind: "storage", - intent_id: id, - } - } - other => BrokerError::LiveHandler(other.to_string()), - })?; - let outcome = if response.status == StorageReconcileStatus::Refused { - CutoverEffectOutcome::Failed - } else { - CutoverEffectOutcome::Succeeded - }; - Ok((outcome, None)) - } - CutoverEffectKind::ResourceStoreCreate => match payload { - Some(CutoverEffectPayload::ZoneStore(request)) => { - let resolver = resolver_ref()?; - let outcome = - crate::live_handlers::live_open_zone_store(resolver, &request.zone_store_id) - .map_err(|err| BrokerError::LiveHandler(err.to_string()))?; - drop(outcome.database_fd); - Ok(( - CutoverEffectOutcome::Succeeded, - Some(d2b_contracts::types::BundleOpId::new( - request.zone_store_id.as_str(), - )), - )) - } - Some(CutoverEffectPayload::StoreSync(request)) => { - let resolver = require_resolver(resolver)?; - let vm_name = lookup_vm_name(resolver, &request.vm_id); - let intent = resolver.find_store_view_intent(&vm_name).ok_or_else(|| { - BrokerError::BundleIntentMissing { - kind: "store-sync-closure", - intent_id: vm_name.clone(), - } - })?; - if intent.intent_id != request.bundle_closure_ref.as_str() { - return Err(BrokerError::BundleIntentMissing { - kind: "store-sync-closure", - intent_id: request.bundle_closure_ref.as_str().to_owned(), - }); - } - crate::ops::store_sync::run_store_sync(intent, &vm_name, request.generation_token) - .map_err(|err| BrokerError::StoreSyncFailed { - error_stage: store_sync_error_kind(err.error_stage()), - message: err.to_string(), - })?; - Ok((CutoverEffectOutcome::Succeeded, None)) - } - _ => Err(BrokerError::Protocol( - "resource-store effect payload kind mismatch".to_owned(), - )), - }, - CutoverEffectKind::ProviderInstall => match payload { - Some(CutoverEffectPayload::Systemd(request)) => { - let resolver = require_resolver(resolver)?; - let (_identity, pidfd) = backend.start_systemd_unit(resolver, &request)?; - drop(pidfd); - Ok((CutoverEffectOutcome::Succeeded, None)) - } - _ => Err(BrokerError::Protocol( - "provider install requires a typed systemd payload".to_owned(), - )), - }, - CutoverEffectKind::ZoneActivation | CutoverEffectKind::GuestActivation => match payload { - Some(CutoverEffectPayload::Activation(request)) => { - let resolver = require_resolver(resolver)?; - let intent = resolver - .find_activation_intent(request.bundle_activation_intent_ref.as_str()) - .ok_or_else(|| BrokerError::BundleIntentMissing { - kind: "activation", - intent_id: request.bundle_activation_intent_ref.as_str().to_owned(), - })?; - if intent.vm != request.vm { - return Err(BrokerError::Protocol( - "cutover activation vm mismatch".to_owned(), - )); - } - let store_view = resolver - .find_store_view_intent(&request.vm) - .ok_or_else(|| BrokerError::BundleIntentMissing { - kind: "store-view", - intent_id: request.vm.clone(), - })?; - backend.run_activation(intent, store_view, request.phase, request.mode)?; - Ok((CutoverEffectOutcome::Succeeded, None)) - } - _ => Err(BrokerError::Protocol( - "zone or guest activation requires a typed activation payload".to_owned(), - )), - }, - CutoverEffectKind::Verification => { - let CutoverEffectPayload::StoreVerify(request) = payload - .ok_or_else(|| BrokerError::Protocol("verify payload required".to_owned()))? - else { - return Err(BrokerError::Protocol( - "verification payload kind mismatch".to_owned(), - )); - }; - if request.repair { - return Err(BrokerError::Protocol( - "verification effect is read-only".to_owned(), - )); - } - let resolver = require_resolver(resolver)?; - let vm_name = lookup_vm_name(resolver, &request.vm_id); - let intent = resolver.find_store_view_intent(&vm_name).ok_or_else(|| { - BrokerError::BundleIntentMissing { - kind: "store-view", - intent_id: vm_name.clone(), - } - })?; - let response = - crate::ops::store_verify::run_store_verify_read_only(intent, request.repair); - let outcome = match response.status { - StoreVerifyStatus::Ok | StoreVerifyStatus::Repaired => { - CutoverEffectOutcome::Succeeded - } - _ => CutoverEffectOutcome::Failed, - }; - Ok((outcome, None)) - } - CutoverEffectKind::QuarantineDestination => { - let CutoverEffectPayload::Quarantine { - staged_id, - source_id, - marker_digest, - } = payload - .ok_or_else(|| BrokerError::Protocol("quarantine payload required".to_owned()))? - else { - return Err(BrokerError::Protocol( - "quarantine payload kind mismatch".to_owned(), - )); - }; - crate::ops::cutover_artifacts::quarantine_staged_destination( - state_dir, - operation_id, - &staged_id, - &source_id, - &marker_digest, - ) - .map_err(|error| BrokerError::LiveHandler(error.to_string()))?; - Ok((CutoverEffectOutcome::Succeeded, None)) - } - CutoverEffectKind::CutoverFinalization => { - let CutoverEffectPayload::Finalization { - artifacts, - disposition_digest, - consent_digest, - } = payload - .ok_or_else(|| BrokerError::Protocol("finalization payload required".to_owned()))? - else { - return Err(BrokerError::Protocol( - "finalization payload kind mismatch".to_owned(), - )); - }; - crate::ops::cutover_artifacts::finalize_legacy_artifacts( - state_dir, - operation_id, - &artifacts, - &disposition_digest, - &consent_digest, - ) - .map_err(|error| BrokerError::LiveHandler(error.to_string()))?; - Ok((CutoverEffectOutcome::Succeeded, None)) - } - CutoverEffectKind::DestroyDurableVolume => { - let CutoverEffectPayload::DestroyDurableVolume { - storage_ref, - marker_digest, - consent_digest, - } = payload.ok_or_else(|| { - BrokerError::Protocol("destructive-volume payload required".to_owned()) - })? - else { - return Err(BrokerError::Protocol( - "destructive-volume payload kind mismatch".to_owned(), - )); - }; - crate::ops::cutover_artifacts::destroy_durable_volume( - state_dir, - operation_id, - &storage_ref, - &marker_digest, - &consent_digest, - ) - .map_err(|error| BrokerError::LiveHandler(error.to_string()))?; - Ok((CutoverEffectOutcome::Succeeded, None)) - } - _ => Err(BrokerError::Unimplemented { - operation: cutover_effect_operation_name(effect), - target_wave: cutover_effect_owner(effect), - }), + BrokerRequest::SpawnRunner { .. } => Err(BrokerError::Unimplemented { + operation: "SpawnRunner", + target_wave: "W4-fu", + }), + BrokerRequest::UpdateHostsFile { .. } => Err(BrokerError::Unimplemented { + operation: "UpdateHostsFile", + target_wave: "W3", + }), + BrokerRequest::UsbipBind { .. } => Err(BrokerError::UnknownOperation { + operation: "UsbipBind", + }), + BrokerRequest::UsbipBindFirewallRule { .. } => Err(BrokerError::Unimplemented { + operation: "UsbipBindFirewallRule", + target_wave: "W3", + }), + BrokerRequest::UsbipExplicitBind { .. } => Err(BrokerError::Unimplemented { + operation: "UsbipExplicitBind", + target_wave: "W5", + }), + BrokerRequest::UsbipExplicitFirewallRule { .. } => Err(BrokerError::Unimplemented { + operation: "UsbipExplicitFirewallRule", + target_wave: "W5", + }), + BrokerRequest::UsbipProxyReconcile { .. } => Err(BrokerError::UnknownOperation { + operation: "UsbipProxyReconcile", + }), + BrokerRequest::UsbipUnbind { .. } => Err(BrokerError::UnknownOperation { + operation: "UsbipUnbind", + }), } } +#[cfg(not(feature = "layer1-bootstrap"))] +fn request_accepts_fd(request: &BrokerRequest) -> bool { + matches!( + request, + BrokerRequest::OpenPeerPidfdFromAcceptedSocket(_) + ) +} + /// Real-wire dispatch. Matches the opaque-ID /// `d2b_contracts_broker::broker_wire::BrokerRequest` tuple-newtype shape and /// wires the live executors into the dispatch arms that have a ready @@ -3229,33 +2113,7 @@ fn dispatch_request_with_backend_and_request_fds( client_version: req.client_version, }, )?; - Ok(DispatchResult::no_fds(hello_ok_response())) - } - RealBrokerRequest::GuestControlSign(req) => { - let response = handle_guest_control_sign(req.clone(), config, resolver)?; - write_success_op_record!( - audit_log, - bundle_metadata, - "GuestControlSign", - req.vm_id.as_str(), - caller_uid, - caller_gid, - &caller_role, - req.vm_id.as_str(), - "guest-control-auth", - tracing_span_id_str(req.tracing_span_id.as_ref()), - OperationFields::GuestControlSign { - vm_id: req.vm_id.as_str().to_owned(), - role: format!("{:?}", req.role), - purpose: format!("{:?}", req.purpose), - transcript_len: guest_control_transcript_len(&req)?, - peer_cid_present: req.peer_cid.is_some(), - capabilities_hash_present: req.capabilities_hash.is_some(), - }, - )?; - Ok(DispatchResult::no_fds(BrokerResponse::GuestControlSign( - response, - ))) + Ok(DispatchResult::no_fds(hello_ok_response(config.profile))) } RealBrokerRequest::ValidateBundle => { // The broker validates the server-configured bundle path. @@ -4192,6 +3050,7 @@ fn dispatch_request_with_backend_and_request_fds( req.resource_uid.as_ref(), req.pid, req.expected_start_time_ticks, + req.guest_execution.as_ref(), ) { return Err(BrokerError::NoPidfd { runner_id }); @@ -4269,6 +3128,7 @@ fn dispatch_request_with_backend_and_request_fds( req.resource_uid.as_ref(), req.pid, req.expected_start_time_ticks, + req.guest_execution.as_ref(), ) }) }); @@ -4346,6 +3206,56 @@ fn dispatch_request_with_backend_and_request_fds( resolved: "nonzero".to_owned(), }); } + match (&req.activation_input, req.role) { + (Some(input), RunnerRole::ActivationNixos) => { + if input.target_generation == 0 { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input.target_generation", + requested: "0".to_owned(), + resolved: "nonzero".to_owned(), + }); + } + let Some(_generation) = req.generation else { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "generation", + requested: "missing".to_owned(), + resolved: "required-for-activation-input".to_owned(), + }); + }; + let encoded = serde_json::to_vec(input).map_err(|_| { + BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: "unserializable".to_owned(), + resolved: "bounded-json".to_owned(), + } + })?; + if encoded.len() + > d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: encoded.len().to_string(), + resolved: d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + .to_string(), + }); + } + } + (Some(_), _) => { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: "present".to_owned(), + resolved: "activation-nixos-role-only".to_owned(), + }); + } + (None, RunnerRole::ActivationNixos) => { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: "missing".to_owned(), + resolved: "required-for-activation-nixos".to_owned(), + }); + } + (None, _) => {} + } if req.resource_ref.is_some() && (req.execution_ref.is_none() || req.execution_domain.is_none() @@ -4596,6 +3506,7 @@ fn dispatch_request_with_backend_and_request_fds( execution_ref: req.execution_ref.clone(), execution_domain: req.execution_domain, user_ref: req.user_ref.clone(), + guest_execution: req.guest_execution.clone(), provider_identity: req.provider_identity, template_identity: req.template_identity, generation: req.generation, @@ -5937,310 +4848,6 @@ fn dispatch_request_with_backend_and_request_fds( }, ))) } - RealBrokerRequest::LaunchCutoverRunner(req) => { - if !caller_role_is_admin(&caller_role) { - audit_log - .write_entry_with_caller_ids( - "LaunchCutoverRunner", - caller_uid, - caller_gid, - "denied-refused", - req.operation_id.as_str(), - "unauthorized", - ) - .map_err(|error| BrokerError::Protocol(error.to_string()))?; - return Err(BrokerError::AuditRequiresAdmin); - } - if req.bootstrap_fd_index != 0 || request_fds.len() != 1 { - return Err(BrokerError::Protocol( - "LaunchCutoverRunner requires exactly one bootstrap fd at index 0".to_owned(), - )); - } - let bootstrap_fd = request_fds - .pop() - .ok_or_else(|| BrokerError::Protocol("cutover bootstrap fd missing".to_owned()))?; - let resolver = require_resolver(resolver)?; - let runner_path = resolver - .bundle - .cutover_runner_path - .as_deref() - .ok_or_else(|| BrokerError::BundleIntentMissing { - kind: "cutover-runner", - intent_id: req.operation_id.to_string(), - })?; - let runner_id = format!("cutover:{}", req.operation_id); - reserve_runner_id_for_spawn(&runner_id)?; - register_cutover_capability( - config, - &req.operation_id, - &req.capability_digest, - req.expires_at_ms, - )?; - let response = match launch_cutover_runner( - Path::new(runner_path), - &config.state_dir, - &req, - bootstrap_fd, - ) { - Ok(response) => response, - Err(error) => { - let _ = remove_cutover_capability(config, &req.operation_id); - return Err(error); - } - }; - if let Err(error) = bind_cutover_capability_runner( - config, - &req.operation_id, - response.pid, - response.start_time_ticks, - ) { - cleanup_registered_runner_after_failure(&runner_id); - let _ = remove_cutover_capability(config, &req.operation_id); - return Err(error); - } - audit_log - .write_entry_with_caller_ids( - "LaunchCutoverRunner", - caller_uid, - caller_gid, - "callable-success", - req.operation_id.as_str(), - "ok", - ) - .map_err(|error| { - cleanup_registered_runner_after_failure(&runner_id); - let _ = remove_cutover_capability(config, &req.operation_id); - BrokerError::Protocol(error.to_string()) - })?; - Ok(DispatchResult::no_fds(BrokerResponse::LaunchCutoverRunner( - response, - ))) - } - RealBrokerRequest::CutoverAudit(req) => { - let d2b_contracts_broker::broker_wire::BrokerCallerRole::CutoverRunner { - operation_id: claimed_operation, - capability_digest, - } = &caller_role - else { - return Err(BrokerError::AuditRequiresAdmin); - }; - if caller_uid != 0 || claimed_operation != &req.operation_id { - return Err(BrokerError::PeerCredentialRefused { - operation: "CutoverAudit", - }); - } - if !cutover_capability_matches(config, &req.operation_id, capability_digest)? { - return Err(BrokerError::PeerCredentialRefused { - operation: "CutoverAudit", - }); - } - let preimage = format!( - "{}:{}:{}:{}:{}", - req.operation_id, - req.phase, - req.transition.as_str(), - req.request_digest.as_str(), - capability_digest.as_str() - ); - let record_id = d2b_contracts_broker::broker_wire::CanonicalAuditDigest::parse( - d2b_contracts_resource::v3::canonical_digest( - "d2b:cutover:audit-record:v1", - preimage.as_bytes(), - ), - ) - .map_err(|_| BrokerError::Protocol("cutover audit record digest invalid".to_owned()))?; - audit_log - .write_entry_with_caller_ids( - "CutoverAudit", - caller_uid, - caller_gid, - req.transition.as_str(), - req.operation_id.as_str(), - "durable", - ) - .map_err(|error| BrokerError::LiveHandler(error.to_string()))?; - Ok(DispatchResult::no_fds(BrokerResponse::CutoverAudit( - d2b_contracts_broker::broker_wire::CutoverAuditResponse { record_id }, - ))) - } - RealBrokerRequest::CutoverEffect(req) => { - let d2b_contracts_broker::broker_wire::BrokerCallerRole::CutoverRunner { - operation_id: claimed_operation, - capability_digest, - } = &caller_role - else { - return Err(BrokerError::AuditRequiresAdmin); - }; - if caller_uid != 0 - || claimed_operation != &req.operation_id - || capability_digest != &req.capability_digest - || !cutover_capability_matches(config, &req.operation_id, capability_digest)? - { - return Err(BrokerError::PeerCredentialRefused { - operation: "CutoverEffect", - }); - } - if !req.authority.permits(req.effect) { - return Err(BrokerError::Protocol( - "cutover effect is not permitted by its authority".to_owned(), - )); - } - let mut verification = None; - let mut admission = None; - let (outcome, identity) = match req.effect { - d2b_contracts_broker::broker_wire::CutoverEffectKind::ApplyAdmission => { - let d2b_contracts_broker::broker_wire::CutoverEffectPayload::ApplyAdmission( - admission_request, - ) = req.payload.clone().ok_or_else(|| { - BrokerError::Protocol("apply admission payload required".to_owned()) - })? - else { - return Err(BrokerError::Protocol( - "apply admission payload kind mismatch".to_owned(), - )); - }; - if req.handoff.is_some() { - return Err(BrokerError::Protocol( - "apply admission does not accept a handoff".to_owned(), - )); - } - let resolver = require_resolver(resolver)?; - let observations = execute_cutover_admission( - &admission_request, - req.authority, - resolver, - &config.activation_helper_path, - ); - let outcome = if observations.predicates_hold { - d2b_contracts_broker::broker_wire::CutoverEffectOutcome::Succeeded - } else { - d2b_contracts_broker::broker_wire::CutoverEffectOutcome::Failed - }; - admission = Some(observations); - (outcome, None) - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::HostDrain => { - if req.payload.is_some() || req.handoff.is_some() { - return Err(BrokerError::Protocol( - "host drain does not accept an effect payload".to_owned(), - )); - } - crate::ops::systemd::stop_cutover_daemon() - .map_err(|error| BrokerError::LiveHandler(error.to_string()))?; - mark_cutover_capability_drained(config, &req.operation_id)?; - ( - d2b_contracts_broker::broker_wire::CutoverEffectOutcome::Succeeded, - None, - ) - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::ClosureActivation => { - let Some(handoff) = req.handoff.clone() else { - return Err(BrokerError::Protocol( - "closure activation requires a typed handoff".to_owned(), - )); - }; - if req.payload.is_some() - || !matches!( - handoff.caller_role, - d2b_contracts_broker::host_generation::HandoffCallerRole::Lifecycle - ) - { - return Err(BrokerError::PeerCredentialRefused { - operation: "CutoverEffect", - }); - } - let handoff_response = backend.apply_host_generation_handoff( - &config.state_dir, - &config.activation_helper_path, - &handoff, - )?; - let outcome = if matches!( - handoff_response.state, - d2b_contracts_broker::host_generation::HandoffState::Completed - | d2b_contracts_broker::host_generation::HandoffState::Transferred - ) { - d2b_contracts_broker::broker_wire::CutoverEffectOutcome::Succeeded - } else { - d2b_contracts_broker::broker_wire::CutoverEffectOutcome::Failed - }; - (outcome, req.identity.clone()) - } - d2b_contracts_broker::broker_wire::CutoverEffectKind::Verification - if matches!( - &req.payload, - Some(d2b_contracts_broker::broker_wire::CutoverEffectPayload::Verification(_)) - ) => - { - let d2b_contracts_broker::broker_wire::CutoverEffectPayload::Verification( - verification_request, - ) = req.payload.clone().ok_or_else(|| { - BrokerError::Protocol("verification payload required".to_owned()) - })? - else { - return Err(BrokerError::Protocol( - "verification payload kind mismatch".to_owned(), - )); - }; - let resolver = require_resolver(resolver)?; - let observations = - execute_cutover_verification(&verification_request, resolver); - let outcome = if observations.zones.iter().all(|zone| zone.healthy) - && observations.sources_preserved - && observations.identity_digests_match - && observations.candidate_current - { - d2b_contracts_broker::broker_wire::CutoverEffectOutcome::Succeeded - } else { - d2b_contracts_broker::broker_wire::CutoverEffectOutcome::Failed - }; - verification = Some(observations); - (outcome, None) - } - effect => execute_cutover_effect_payload( - effect, - req.payload.clone(), - resolver, - backend, - &config.state_dir, - &req.operation_id, - )?, - }; - let preimage = format!( - "{}:{}:{}:{}:{}:{}", - req.operation_id, - req.phase, - req.effect_id, - req.effect as u8, - req.request_digest.as_str(), - capability_digest.as_str() - ); - let audit_record_id = d2b_contracts_broker::broker_wire::CanonicalAuditDigest::parse( - d2b_contracts_resource::v3::canonical_digest( - "d2b:cutover:effect-audit:v1", - preimage.as_bytes(), - ), - ) - .map_err(|_| BrokerError::Protocol("cutover effect audit digest invalid".to_owned()))?; - audit_log - .write_entry_with_caller_ids( - "CutoverEffect", - caller_uid, - caller_gid, - "effect-completed", - req.operation_id.as_str(), - "durable", - ) - .map_err(|error| BrokerError::LiveHandler(error.to_string()))?; - Ok(DispatchResult::no_fds(BrokerResponse::CutoverEffect( - d2b_contracts_broker::broker_wire::CutoverEffectResponse { - outcome, - identity, - verification, - admission, - audit_record_id, - }, - ))) - } RealBrokerRequest::ApplyHostGenerationHandoff(req) => { let target = req.target.to_canonical_string(); let fields = OperationFields::ApplyHostGenerationHandoff { @@ -7241,7 +5848,6 @@ fn caller_role_authz_result(caller_role: &CallerRole) -> &'static str { match caller_role { CallerRole::AdminUid { .. } | CallerRole::RootUid { .. } => "admin", CallerRole::LauncherUid { .. } => "launcher", - CallerRole::CutoverRunner { .. } => "d2b-cutover-runner", CallerRole::NotAuthorized => "deny", } } @@ -7365,193 +5971,6 @@ fn write_success_op_record_impl( ) } -#[cfg(not(feature = "layer1-bootstrap"))] -type GuestControlHmac = Hmac; - -#[cfg(not(feature = "layer1-bootstrap"))] -fn handle_guest_control_sign( - req: d2b_contracts_broker::broker_wire::GuestControlSignRequest, - config: &ServerConfig, - resolver: Option<&Arc>, -) -> Result { - req.validate_shape() - .map_err(|reason| BrokerError::GuestControlSignRefused { reason })?; - let resolver = resolver.ok_or(BrokerError::BundleResolverUnavailable)?; - if resolver.find_manifest_vm(req.vm_id.as_str()).is_none() { - return Err(BrokerError::GuestControlSignRefused { - reason: "vm-not-in-bundle", - }); - } - let transcript = guest_control_transcript(&req)?; - let mut token = read_guest_control_token(&config.state_dir, req.vm_id.as_str())?; - let mut mac = GuestControlHmac::new_from_slice(&token).map_err(|_| { - BrokerError::GuestControlSignRefused { - reason: "token-unavailable", - } - })?; - mac.update(&transcript); - let tag = mac.finalize().into_bytes().to_vec(); - token.fill(0); - Ok(d2b_contracts_broker::broker_wire::GuestControlSignResponse { tag }) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn guest_control_transcript_len( - req: &d2b_contracts_broker::broker_wire::GuestControlSignRequest, -) -> Result { - Ok(guest_control_transcript(req)?.len()) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn guest_control_transcript( - req: &d2b_contracts_broker::broker_wire::GuestControlSignRequest, -) -> Result, BrokerError> { - use d2b_contracts_broker::broker_wire::GuestControlProofRole; - use d2b_contracts_control::guest_auth::{ - self, AUTH_NONCE_LEN, AuthDirection, AuthPurpose, GUEST_CONTROL_AUTH_PORT, - GuestAuthTranscript, ProofRole, - }; - req.validate_shape() - .map_err(|reason| BrokerError::GuestControlSignRefused { reason })?; - if req.protocol_version != d2b_contracts_control::guest_wire::GUEST_CONTROL_PROTOCOL_VERSION - || req.guest_control_port != GUEST_CONTROL_AUTH_PORT - { - return Err(BrokerError::GuestControlSignRefused { - reason: "protocol-or-port", - }); - } - let host_nonce: [u8; AUTH_NONCE_LEN] = - req.host_nonce - .as_slice() - .try_into() - .map_err(|_| BrokerError::GuestControlSignRefused { - reason: "host-nonce-length", - })?; - let guest_nonce: [u8; AUTH_NONCE_LEN] = - req.guest_nonce.as_slice().try_into().map_err(|_| { - BrokerError::GuestControlSignRefused { - reason: "guest-nonce-length", - } - })?; - let role = match req.role { - GuestControlProofRole::HostProof => ProofRole::Host, - GuestControlProofRole::GuestProof => ProofRole::Guest, - }; - Ok(guest_auth::encode_transcript(&GuestAuthTranscript { - role, - direction: AuthDirection::HostToGuest, - purpose: AuthPurpose::GuestControlAuthV1, - vm_id: req.vm_id.as_str(), - protocol_version: req.protocol_version, - guest_control_port: req.guest_control_port, - peer_cid: req.peer_cid, - host_nonce: &host_nonce, - guest_nonce: &guest_nonce, - guest_boot_id: req.guest_boot_id.as_str(), - capabilities_hash: req.capabilities_hash.as_deref().map(str::as_bytes), - })) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn read_guest_control_token(state_dir: &Path, vm_id: &str) -> Result, BrokerError> { - const MAX_TOKEN_BYTES: usize = 4096; - let token_path = state_dir.join(format!("guest-control-{vm_id}/token")); - validate_guest_control_token_path(state_dir, &token_path)?; - let file = nix::fcntl::open( - token_path.as_path(), - nix::fcntl::OFlag::O_RDONLY | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_NOFOLLOW, - nix::sys::stat::Mode::empty(), - ) - .map_err(|_| BrokerError::GuestControlSignRefused { - reason: "token-open", - })?; - let mut file = fs::File::from(owned_fd_from_raw(file)); - let mut token = Vec::new(); - std::io::Read::by_ref(&mut file) - .take((MAX_TOKEN_BYTES + 1) as u64) - .read_to_end(&mut token) - .map_err(|_| BrokerError::GuestControlSignRefused { - reason: "token-read", - })?; - let metadata = file - .metadata() - .map_err(|_| BrokerError::GuestControlSignRefused { - reason: "token-metadata", - })?; - if !metadata.is_file() - || !owner_is_safe_for_guest_control_token(metadata.uid()) - || !matches!(metadata.mode() & 0o777, 0o400 | 0o440) - || token.is_empty() - || token.len() > MAX_TOKEN_BYTES - { - return Err(BrokerError::GuestControlSignRefused { - reason: "token-unsafe", - }); - } - while matches!(token.last(), Some(b'\n' | b'\r')) { - token.pop(); - } - if token.is_empty() { - return Err(BrokerError::GuestControlSignRefused { - reason: "token-empty", - }); - } - Ok(token) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn validate_guest_control_token_path( - state_dir: &Path, - token_path: &Path, -) -> Result<(), BrokerError> { - if !state_dir.is_absolute() - || state_dir == Path::new("/nix/store") - || state_dir.starts_with("/nix/store/") - || !token_path.starts_with(state_dir) - { - return Err(BrokerError::GuestControlSignRefused { - reason: "token-path", - }); - } - let mut current = PathBuf::new(); - let parent = token_path - .parent() - .ok_or(BrokerError::GuestControlSignRefused { - reason: "token-parent", - })?; - for component in parent.components() { - current.push(component); - let metadata = - fs::symlink_metadata(¤t).map_err(|_| BrokerError::GuestControlSignRefused { - reason: "token-parent", - })?; - if metadata.file_type().is_symlink() - || !metadata.is_dir() - || !owner_is_safe_for_guest_control_token(metadata.uid()) - || metadata.mode() & 0o022 != 0 - { - return Err(BrokerError::GuestControlSignRefused { - reason: "token-parent-unsafe", - }); - } - } - let metadata = - fs::symlink_metadata(token_path).map_err(|_| BrokerError::GuestControlSignRefused { - reason: "token-missing", - })?; - if metadata.file_type().is_symlink() { - return Err(BrokerError::GuestControlSignRefused { - reason: "token-symlink", - }); - } - Ok(()) -} - -#[cfg(not(feature = "layer1-bootstrap"))] -fn owner_is_safe_for_guest_control_token(uid: u32) -> bool { - uid == 0 || cfg!(test) -} - #[cfg(not(feature = "layer1-bootstrap"))] fn activation_mode_name(mode: d2b_contracts_broker::broker_wire::ActivationMode) -> &'static str { match mode { @@ -7609,6 +6028,7 @@ struct RunnerRegistration { start_time_ticks: u64, binary_path: PathBuf, cgroup_subtree: String, + guest_execution: Option, } #[cfg(not(feature = "layer1-bootstrap"))] @@ -7641,6 +6061,7 @@ fn register_runner_metadata( start_time_ticks, binary_path: intent.binary_path.clone(), cgroup_subtree: intent.cgroup_placement.subtree.clone(), + guest_execution: request.guest_execution.clone(), }, ); Ok(()) @@ -7675,6 +6096,7 @@ fn register_runner_metadata_from_open( start_time_ticks: request.expected_start_time_ticks, binary_path: intent.binary_path.clone(), cgroup_subtree: intent.cgroup_placement.subtree.clone(), + guest_execution: request.guest_execution.clone(), }, ); Ok(()) @@ -7713,11 +6135,54 @@ fn registration_matches( resource_uid: Option<&d2b_contracts_resource::v3::ResourceUid>, pid: Option, start_time_ticks: Option, + guest_execution: Option< + &d2b_contracts_broker::broker_wire::GuestExecutionBinding, + >, ) -> bool { registration.resource_ref.as_ref() == resource_ref && registration.resource_uid.as_ref() == resource_uid && pid.is_none_or(|pid| registration.pid == pid) && start_time_ticks.is_none_or(|ticks| registration.start_time_ticks == ticks) + && registration.guest_execution.as_ref() == guest_execution +} + +#[cfg(not(feature = "layer1-bootstrap"))] +fn rebind_guest_execution_registration( + registration: &mut RunnerRegistration, + requested: Option<&d2b_contracts_broker::broker_wire::GuestExecutionBinding>, +) -> Result { + if requested.is_some_and(|binding| !binding.is_valid()) { + return Err(BrokerError::LiveHandler( + "guest runner execution binding changed".to_owned(), + )); + } + if registration.guest_execution.as_ref() == requested { + return Ok(false); + } + + let (Some(current), Some(next)) = (registration.guest_execution.as_ref(), requested) else { + return Err(BrokerError::LiveHandler( + "guest runner execution binding changed".to_owned(), + )); + }; + if current.target_uid != next.target_uid + || current.boot_identity_digest != next.boot_identity_digest + || current.assignment_epoch != next.assignment_epoch + || current.provider_generation != next.provider_generation + || current.controller_generation != next.controller_generation + { + return Err(BrokerError::LiveHandler( + "guest runner execution binding changed".to_owned(), + )); + } + if next.session_generation <= current.session_generation { + return Err(BrokerError::LiveHandler( + "guest runner session generation is not newer".to_owned(), + )); + } + + registration.guest_execution = Some(next.clone()); + Ok(true) } #[cfg(not(feature = "layer1-bootstrap"))] @@ -7738,79 +6203,110 @@ fn observe_registered_runner( request.resource_ref.as_ref(), request.resource_uid.as_ref(), ); - let registration = { - let registry = runner_metadata_registry().lock().map_err(|_| { + let registered = (|| -> Result< + Option, + BrokerError, + > { + // Keep the lock order aligned with deregistration: pidfd registry + // first, metadata second. This makes the binding update atomic with + // the live pidfd registration. + let pidfd_registry = runner_pidfd_registry().lock().map_err(|_| { + BrokerError::Protocol("runner pidfd registry mutex poisoned".to_owned()) + })?; + let mut metadata_registry = runner_metadata_registry().lock().map_err(|_| { BrokerError::Protocol("runner metadata registry mutex poisoned".to_owned()) })?; - registry.get(&runner_id).cloned() - }; - let Some(registration) = registration else { - return discover_runner_candidate(request, intent); - }; - - let pidfd_registered = runner_pidfd_registry() - .lock() - .map_err(|_| BrokerError::Protocol("runner pidfd registry mutex poisoned".to_owned()))? - .contains_key(&runner_id); - if !pidfd_registered - || registration.vm_id != request.vm_id.as_str() - || registration.role_id != request.role_id.as_str() - || registration.role != request.role - || !registration_matches( - ®istration, - request.resource_ref.as_ref(), - request.resource_uid.as_ref(), - None, - None, - ) - || registration.bundle_runner_intent_ref != request.bundle_runner_intent_ref.as_str() - || registration.pid <= 0 - || registration.start_time_ticks == 0 - || registration.binary_path != intent.binary_path - || registration.cgroup_subtree != intent.cgroup_placement.subtree - { - return discover_runner_candidate(request, intent); - } - - let Some(start_time_ticks) = read_proc_start_time_ticks(registration.pid)? else { - remove_runner_metadata(&runner_id); - return Ok(d2b_contracts_broker::broker_wire::ObserveRunnerResponse { - vm_id: request.vm_id.clone(), - role_id: request.role_id.clone(), - present: false, - pid: 0, - start_time_ticks: 0, - cgroup_verified: false, - executable_verified: false, - }); - }; - if start_time_ticks != registration.start_time_ticks { - return Err(BrokerError::LiveHandler( - "runner process identity changed".to_owned(), - )); - } - let cgroup_verified = proc_cgroup_matches(registration.pid, ®istration.cgroup_subtree); - let executable_path = read_runner_executable(registration.pid); - let executable_verified = match executable_path { - Ok(ref path) => executable_paths_match(path, ®istration.binary_path), - Err(ref error) if error.kind() == io::ErrorKind::PermissionDenied => { - // A registered pidfd and the broker-owned cgroup are the - // executable provenance established by this broker. Linux - // resets dumpability when a runner changes uid, so Yama blocks - // the redundant /proc//exe read without CAP_SYS_PTRACE. - pidfd_registered && cgroup_verified - } - Err(_) => false, - }; - Ok(d2b_contracts_broker::broker_wire::ObserveRunnerResponse { - vm_id: request.vm_id.clone(), - role_id: request.role_id.clone(), - present: true, - pid: registration.pid, - start_time_ticks, - cgroup_verified, - executable_verified, - }) + let registration = match metadata_registry.get(&runner_id).cloned() { + Some(registration) => registration, + None => return Ok(None), + }; + let mut observed_registration = registration.clone(); + let rebound = rebind_guest_execution_registration( + &mut observed_registration, + request.guest_execution.as_ref(), + )?; + let pidfd_registered = pidfd_registry.contains_key(&runner_id); + if !pidfd_registered + || observed_registration.vm_id != request.vm_id.as_str() + || observed_registration.role_id != request.role_id.as_str() + || observed_registration.role != request.role + || !registration_matches( + &observed_registration, + request.resource_ref.as_ref(), + request.resource_uid.as_ref(), + None, + None, + request.guest_execution.as_ref(), + ) + || observed_registration.bundle_runner_intent_ref + != request.bundle_runner_intent_ref.as_str() + || observed_registration.pid <= 0 + || observed_registration.start_time_ticks == 0 + || observed_registration.binary_path != intent.binary_path + || observed_registration.cgroup_subtree != intent.cgroup_placement.subtree + { + Ok(None) + } else { + let Some(start_time_ticks) = read_proc_start_time_ticks(observed_registration.pid)? + else { + metadata_registry.remove(&runner_id); + return Ok(Some( + d2b_contracts_broker::broker_wire::ObserveRunnerResponse { + vm_id: request.vm_id.clone(), + role_id: request.role_id.clone(), + present: false, + pid: 0, + start_time_ticks: 0, + cgroup_verified: false, + executable_verified: false, + }, + )); + }; + if start_time_ticks != observed_registration.start_time_ticks { + return Err(BrokerError::LiveHandler( + "runner process identity changed".to_owned(), + )); + } + let cgroup_verified = proc_cgroup_matches( + observed_registration.pid, + &observed_registration.cgroup_subtree, + ); + let executable_path = read_runner_executable(observed_registration.pid); + let executable_verified = match executable_path { + Ok(ref path) => executable_paths_match(path, &observed_registration.binary_path), + Err(ref error) if error.kind() == io::ErrorKind::PermissionDenied => { + // A registered pidfd and the broker-owned cgroup are the + // executable provenance established by this broker. Linux + // resets dumpability when a runner changes uid, so Yama + // blocks the redundant /proc//exe read without + // CAP_SYS_PTRACE. + pidfd_registered && cgroup_verified + } + Err(_) => false, + }; + if rebound { + if let Some(stored) = metadata_registry.get_mut(&runner_id) { + stored.guest_execution = observed_registration.guest_execution.clone(); + } else { + return Err(BrokerError::LiveHandler( + "runner registration disappeared".to_owned(), + )); + } + } + Ok(Some( + d2b_contracts_broker::broker_wire::ObserveRunnerResponse { + vm_id: request.vm_id.clone(), + role_id: request.role_id.clone(), + present: true, + pid: observed_registration.pid, + start_time_ticks, + cgroup_verified, + executable_verified, + }, + )) + } + })()?; + registered.map_or_else(|| discover_runner_candidate(request, intent), Ok) } #[cfg(not(feature = "layer1-bootstrap"))] @@ -8682,8 +7178,12 @@ impl DispatchBackend for LiveDispatchBackend { self.daemon_uid, self.daemon_gid, )?; - let mut outcome = crate::live_handlers::live_spawn_runner(plan_input, preopened.child_fds) - .map_err(|err| { + let mut outcome = crate::live_handlers::live_spawn_runner( + plan_input, + preopened.child_fds, + req.activation_input.as_ref(), + ) + .map_err(|err| { // Log the actual LiveHandlerError detail before wrapping it // in the opaque BrokerError::LiveHandler envelope so // operators can see WHY the spawn failed in journalctl. @@ -10497,19 +8997,67 @@ fn runner_role_for_process_role( ProcessRole::Audio => Some(RunnerRole::Audio), ProcessRole::CloudHypervisorRunner => Some(RunnerRole::CloudHypervisor), ProcessRole::QemuMediaRunner => Some(RunnerRole::QemuMedia), + ProcessRole::ActivationNixosRunner => Some(RunnerRole::ActivationNixos), ProcessRole::VsockRelay => Some(RunnerRole::VsockRelay), ProcessRole::OtelHostBridge => Some(RunnerRole::OtelHostBridge), ProcessRole::Usbip => Some(RunnerRole::Usbip), ProcessRole::WaylandProxy => Some(RunnerRole::WaylandProxy), ProcessRole::HostReconcile | ProcessRole::StoreVirtiofsPreflight - | ProcessRole::GuestSshReadiness - | ProcessRole::GuestControlHealth + | ProcessRole::ComponentSessionHealth | ProcessRole::SecurityKeyFrontend => None, } } #[cfg(not(feature = "layer1-bootstrap"))] +fn validate_guest_process_binding( + request: &d2b_contracts_broker::broker_wire::BrokerRequest, +) -> Result<(), BrokerError> { + use d2b_contracts_broker::broker_wire::BrokerRequest; + + let binding = match request { + BrokerRequest::SpawnRunner(request) => request.guest_execution.as_ref(), + BrokerRequest::OpenPidfd(request) => request.guest_execution.as_ref(), + BrokerRequest::ObserveRunner(request) => request.guest_execution.as_ref(), + BrokerRequest::StartSystemdUnit(request) + | BrokerRequest::ObserveSystemdUnit(request) + | BrokerRequest::CheckSystemdUserManager(request) => request.guest_execution.as_ref(), + BrokerRequest::OpenSystemdUnitPidfd(request) => request.unit.guest_execution.as_ref(), + BrokerRequest::StopSystemdUnit(request) => request.unit.guest_execution.as_ref(), + BrokerRequest::SignalRunner(request) => request.guest_execution.as_ref(), + BrokerRequest::DeregisterRunnerPidfd(request) => request.guest_execution.as_ref(), + _ => None, + }; + let Some(binding) = binding else { + return Ok(()); + }; + if !binding.is_valid() { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "guest_execution", + requested: "invalid".to_owned(), + resolved: "nonzero-target-session-boot-assignment-generations".to_owned(), + }); + } + let boot_id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") + .map_err(|_| BrokerError::SpawnRunnerIntentMismatch { + field: "guest_execution.boot_identity_digest", + requested: "present".to_owned(), + resolved: "kernel-boot-identity-unavailable".to_owned(), + })?; + let mut digest = Sha256::new(); + digest.update(b"d2b-kernel-boot-id-v1\0"); + digest.update(boot_id.trim().as_bytes()); + let expected: [u8; 32] = digest.finalize().into(); + if binding.boot_identity_digest != expected { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "guest_execution.boot_identity_digest", + requested: "mismatch".to_owned(), + resolved: "kernel-boot-identity".to_owned(), + }); + } + Ok(()) +} + #[cfg(not(feature = "layer1-bootstrap"))] fn validate_sandbox_launch_plan( req: &d2b_contracts_broker::broker_wire::SpawnRunnerRequest, @@ -10555,8 +9103,7 @@ fn validate_sandbox_launch_plan( resolved: "unsupported".to_owned(), }); } - if plan.environment_class != d2b_contracts_resource::v3::process::EnvironmentClass::Minimal - { + if plan.environment_class != d2b_contracts_resource::v3::process::EnvironmentClass::Minimal { return Err(BrokerError::SpawnRunnerIntentMismatch { field: "sandbox_plan.environment_class", requested: format!("{:?}", plan.environment_class), @@ -10659,9 +9206,7 @@ fn capability_matches( capability: &str, ) -> bool { let expected = match class { - d2b_contracts_resource::v3::process::CapabilityClass::NetworkBind => { - "CAP_NET_BIND_SERVICE" - } + d2b_contracts_resource::v3::process::CapabilityClass::NetworkBind => "CAP_NET_BIND_SERVICE", d2b_contracts_resource::v3::process::CapabilityClass::NetworkRaw => "CAP_NET_RAW", d2b_contracts_resource::v3::process::CapabilityClass::NetworkAdmin => "CAP_NET_ADMIN", d2b_contracts_resource::v3::process::CapabilityClass::SysTime => "CAP_SYS_TIME", @@ -10765,6 +9310,66 @@ fn validate_spawn_runner_request_matches_intent( resolved: expected_role.as_str().to_owned(), }); } + if req.resource_ref.is_some() { + let provider_identity_matches = ["system-minijail", "system-systemd"] + .into_iter() + .map(|provider| { + let mut digest = Sha256::new(); + digest.update(b"d2b-process-provider-v1"); + digest.update(provider.as_bytes()); + let digest: [u8; 32] = digest.finalize().into(); + digest + }) + .any(|digest| req.provider_identity == Some(digest)); + if !provider_identity_matches { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "provider_identity", + requested: "mismatch".to_owned(), + resolved: "signed-process-provider".to_owned(), + }); + } + let template_matches = [ + Some(intent.role_id.as_str()), + Some(intent.profile_id.as_str()), + match intent.role { + d2b_core::processes::ProcessRole::SwtpmPreStartFlush => Some("swtpm-flush"), + d2b_core::processes::ProcessRole::Swtpm => Some("swtpm"), + d2b_core::processes::ProcessRole::Virtiofsd => Some("virtiofsd"), + d2b_core::processes::ProcessRole::Video => Some("video"), + d2b_core::processes::ProcessRole::Gpu => Some("gpu"), + d2b_core::processes::ProcessRole::GpuRenderNode => Some("gpu-render-node"), + d2b_core::processes::ProcessRole::Audio => Some("audio"), + d2b_core::processes::ProcessRole::CloudHypervisorRunner => { + Some("cloud-hypervisor") + } + d2b_core::processes::ProcessRole::QemuMediaRunner => Some("qemu-media"), + d2b_core::processes::ProcessRole::ActivationNixosRunner => { + Some("activation-nixos-runner") + } + d2b_core::processes::ProcessRole::VsockRelay => Some("vsock-relay"), + d2b_core::processes::ProcessRole::OtelHostBridge => Some("otel-host-bridge"), + d2b_core::processes::ProcessRole::Usbip => Some("usbip"), + d2b_core::processes::ProcessRole::WaylandProxy => Some("wayland-proxy"), + _ => None, + }, + ] + .into_iter() + .flatten() + .any(|template| { + let mut digest = Sha256::new(); + digest.update(b"d2b-process-template-v1"); + digest.update(template.as_bytes()); + let digest: [u8; 32] = digest.finalize().into(); + req.template_identity == Some(digest) + }); + if !template_matches { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "template_identity", + requested: "mismatch".to_owned(), + resolved: "signed-process-template".to_owned(), + }); + } + } Ok(()) } @@ -11733,59 +10338,6 @@ fn caller_role_is_admin(caller_role: &CallerRole) -> bool { } } -#[cfg(not(feature = "layer1-bootstrap"))] -fn launch_cutover_runner( - runner_path: &Path, - state_dir: &Path, - request: &d2b_contracts_broker::broker_wire::LaunchCutoverRunnerRequest, - bootstrap_fd: OwnedFd, -) -> Result { - if !runner_path.is_absolute() - || !runner_path - .to_str() - .is_some_and(|path| path.starts_with("/nix/store/")) - { - return Err(BrokerError::LiveHandler( - "cutover runner path is not a trusted store path".to_owned(), - )); - } - let outcome = crate::sys::pidfd_sys::spawn_cutover_runner( - runner_path, - state_dir, - Path::new(DEFAULT_CUTOVER_SOCKET_ROOT), - bootstrap_fd.as_raw_fd(), - ) - .map_err(|error| BrokerError::LiveHandler(format!("cutover runner spawn: {error}")))?; - let pid = outcome.pid; - let runner_id = format!("cutover:{}", request.operation_id); - let start_time_ticks = match read_proc_start_time_ticks(pid) { - Ok(Some(start_time_ticks)) => start_time_ticks, - Ok(None) => { - cleanup_spawned_runner_after_failure(&runner_id, outcome.pidfd.as_fd()); - return Err(BrokerError::LiveHandler( - "cutover runner exited before identity read".to_owned(), - )); - } - Err(error) => { - cleanup_spawned_runner_after_failure(&runner_id, outcome.pidfd.as_fd()); - return Err(error); - } - }; - if let Err(error) = register_runner_pidfd(&runner_id, &outcome.pidfd) { - cleanup_spawned_runner_after_failure(&runner_id, outcome.pidfd.as_fd()); - return Err(error); - } - // Close the registration race for a runner that exits before the - // background SIGCHLD loop observes the pidfd. - targeted_reap_runner(&runner_id, outcome.pidfd.as_fd()); - Ok(d2b_contracts_broker::broker_wire::LaunchCutoverRunnerResponse { - operation_id: request.operation_id.clone(), - pid, - start_time_ticks, - pidfd_index: None, - }) -} - impl BrokerError { #[allow(clippy::too_many_arguments)] fn audit( @@ -11832,6 +10384,7 @@ impl BrokerError { | Self::StoreSyncFailed { .. } | Self::SwtpmDirHardening { .. } | Self::RequestValidation { .. } + | Self::ProfileOperationRefused { .. } ); let has_request_payload = !audit_context .request_fields @@ -12124,7 +10677,7 @@ impl BrokerError { // stderr) in the broker journal, not just the // `Broker.LiveHandlerFailed` wrapper kind. The same // detail is recorded in the audit log; an operator - // reading `journalctl -u d2b-priv-broker` should not + // reading `journalctl -u d2b-broker` should not // have to cross-reference the audit jsonl to learn why a // runner spawn failed. tracing::warn!( @@ -12154,6 +10707,37 @@ impl BrokerError { message, )?; } + Self::ProfileOperationRefused { profile, operation } => { + audit_log.write_entry_with_caller_ids( + operation, + caller_uid, + caller_gid, + "profile-operation-denied", + opaque_target_id, + "denied", + )?; + audit_log.record_with_join( + operation, + opaque_target_id, + caller_uid, + caller_gid, + audit_context.peer_pid, + audit_context.peer_role.as_str(), + authz_result, + "", + opaque_target_id, + audit_context.verb.as_str(), + audit_context.request_fields.clone(), + "denied-refused", + Some("profile-operation-denied"), + None, + bundle_metadata.bundle_version, + bundle_metadata.bundle_hash, + audit_context.duration_us(), + Some(serde_json::json!({ "profile": profile.as_str() })), + supplied_join, + )?; + } Self::CoexistenceRefused { manager, rationale } => { audit_log.write_entry_with_caller_ids( operation, @@ -12317,17 +10901,6 @@ impl BrokerError { ), )?; } - Self::GuestControlSignRefused { reason } => { - audit_log.write_error_entry_with_caller_ids( - operation, - caller_uid, - caller_gid, - "guest-control-sign-refused", - opaque_target_id, - "Broker.GuestControlSignRefused", - reason, - )?; - } // The StoreSync dispatch arm already wrote the signed terminal // `OperationFields::StoreSync` record (ADR 0027: exactly one // terminal record per attempt). Writing the generic error entry @@ -12543,14 +11116,17 @@ impl BrokerError { operation, None, "broker peer credential check refused the private request", - "Ensure only d2bd connects to d2b-priv-broker.socket; restart d2bd after host credential changes.", + "Ensure only d2bd connects to d2b-broker.socket; restart d2bd after host credential changes.", ), - Self::GuestControlSignRefused { reason } => error_response( - "Broker.GuestControlSignRefused", - "GuestControlSign", - Some("W11"), - reason, - "Check guest-control token materialization and the structured auth transcript fields.", + Self::ProfileOperationRefused { profile, operation } => error_response( + "Broker.ProfileOperationDenied", + operation, + None, + &format!( + "broker profile `{}` does not admit this effect class", + profile.as_str() + ), + "Use the fixed effect adapter for the authority that owns this operation; the active broker profile cannot be changed by a request.", ), Self::OtelHostBridgeIntentInvalid { intent_vm, @@ -12640,13 +11216,24 @@ fn public_protocol_message(message: &str) -> String { } } -fn hello_ok_response() -> BrokerResponse { +fn profile_capabilities(profile: BrokerProfile) -> Vec { + match profile { + BrokerProfile::Host => CAPABILITIES.iter().map(|item| (*item).to_owned()).collect(), + BrokerProfile::Guest => profile + .operations() + .iter() + .map(|item| (*item).to_owned()) + .collect(), + } +} + +fn hello_ok_response(profile: BrokerProfile) -> BrokerResponse { #[cfg(feature = "layer1-bootstrap")] { BrokerResponse::HelloOk { server_version: "0.0.0-w2-bootstrap".to_owned(), selected_version: "0.0.0-test".to_owned(), - capabilities: CAPABILITIES.iter().map(|item| (*item).to_owned()).collect(), + capabilities: profile_capabilities(profile), } } #[cfg(not(feature = "layer1-bootstrap"))] @@ -12654,7 +11241,7 @@ fn hello_ok_response() -> BrokerResponse { BrokerResponse::Hello(d2b_contracts_broker::broker_wire::HelloResponse { server_version: "0.0.0-w2".to_owned(), selected_version: "0.0.0-w2".to_owned(), - capabilities: CAPABILITIES.iter().map(|item| (*item).to_owned()).collect(), + capabilities: profile_capabilities(profile), }) } } @@ -12762,10 +11349,7 @@ fn redact_public_detail(detail: &str) -> String { { return detail.to_owned(); } - d2b_contracts_resource::v3::canonical_digest( - "d2b:broker-public-error:v1", - detail.as_bytes(), - ) + d2b_contracts_resource::v3::canonical_digest("d2b:broker-public-error:v1", detail.as_bytes()) } /// Start a background tokio runtime that listens for SIGCHLD and reaps @@ -13108,7 +11692,8 @@ mod tests { use d2b_contracts::types::BundleOpId; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_contracts_broker::broker_wire::{ - ActivationMode, ActivationPhase, RunActivationRequest, RunActivationResponse, + ActivationMode, ActivationPhase, GuestExecutionBinding, RunActivationRequest, + RunActivationResponse, }; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_core::bundle_resolver::{ResolvedActivationIntent, ResolvedStoreViewIntent}; @@ -13223,7 +11808,7 @@ mod tests { (ProcessRole::Usbip, Some(RunnerRole::Usbip)), (ProcessRole::HostReconcile, None), (ProcessRole::StoreVirtiofsPreflight, None), - (ProcessRole::GuestSshReadiness, None), + (ProcessRole::ComponentSessionHealth, None), ]; for (role, expected) in cases { @@ -13231,6 +11816,107 @@ mod tests { } } + #[cfg(not(feature = "layer1-bootstrap"))] + fn guest_execution_binding(session_generation: u64) -> GuestExecutionBinding { + GuestExecutionBinding { + target_uid: d2b_contracts_resource::v3::ResourceUid::parse( + "123e4567-e89b-42d3-a456-426614174000", + ) + .expect("Guest UID"), + boot_identity_digest: [7; 32], + session_generation, + assignment_epoch: 3, + provider_generation: 4, + controller_generation: 5, + } + } + + #[cfg(not(feature = "layer1-bootstrap"))] + fn guest_runner_registration(binding: GuestExecutionBinding) -> RunnerRegistration { + RunnerRegistration { + vm_id: "guest-vm".to_owned(), + role_id: "guest-process".to_owned(), + resource_ref: None, + resource_uid: None, + role: d2b_contracts_broker::broker_wire::RunnerRole::CloudHypervisor, + bundle_runner_intent_ref: "runner:guest-vm:guest-process".to_owned(), + pid: 1, + start_time_ticks: 1, + binary_path: PathBuf::from("/bin/true"), + cgroup_subtree: "d2b.slice/guest-vm/guest-process".to_owned(), + guest_execution: Some(binding), + } + } + + #[cfg(not(feature = "layer1-bootstrap"))] + #[test] + fn guest_runner_registration_rebinds_to_a_newer_session() { + let mut registration = guest_runner_registration(guest_execution_binding(1)); + let next = guest_execution_binding(2); + + assert!(rebind_guest_execution_registration(&mut registration, Some(&next)) + .expect("newer session generation rebinds")); + assert_eq!(registration.guest_execution, Some(next.clone())); + assert!( + !rebind_guest_execution_registration(&mut registration, Some(&next)) + .expect("same binding remains an exact match") + ); + } + + #[cfg(not(feature = "layer1-bootstrap"))] + #[test] + fn guest_runner_registration_rejects_stale_or_changed_bindings() { + for generation in [1, 0] { + let mut registration = guest_runner_registration(guest_execution_binding(2)); + let stale = guest_execution_binding(generation); + assert!( + rebind_guest_execution_registration(&mut registration, Some(&stale)).is_err(), + "session generation {generation} must not replace generation 2" + ); + } + + let mut registration = guest_runner_registration(guest_execution_binding(2)); + for changed in [ + { + let mut binding = guest_execution_binding(3); + binding.target_uid = d2b_contracts_resource::v3::ResourceUid::parse( + "223e4567-e89b-42d3-a456-426614174000", + ) + .expect("Guest UID"); + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.boot_identity_digest = [8; 32]; + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.assignment_epoch = 4; + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.provider_generation = 5; + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.controller_generation = 6; + binding + }, + ] { + assert!( + rebind_guest_execution_registration(&mut registration, Some(&changed)).is_err(), + "non-session binding changes must not be accepted as a reconnect" + ); + assert_eq!( + registration.guest_execution, + Some(guest_execution_binding(2)) + ); + } + } + #[cfg(not(feature = "layer1-bootstrap"))] #[test] fn qemu_media_enroll_request_fields_redact_raw_busid() { @@ -13471,17 +12157,17 @@ mod tests { #[allow(clippy::infallible_destructuring_match)] fn parse_command_accepts_realm_controllers_path_without_changing_socket_defaults() { let mode = parse_command([ - "serve".to_owned(), + "host".to_owned(), "--realm-controllers-path".to_owned(), "/etc/d2b/custom-realm-controllers.json".to_owned(), "--realm-identity-path".to_owned(), "/etc/d2b/custom-realm-identity.json".to_owned(), "--test-mode".to_owned(), ]) - .expect("serve command parses"); + .expect("host command parses"); let config = match mode { - BrokerMode::Serve(config) => config, + BrokerMode::Host(config) | BrokerMode::Guest(config) => config, #[cfg(feature = "layer1-bootstrap")] other => panic!("expected serve mode, got {other:?}"), }; @@ -13502,11 +12188,11 @@ mod tests { // Rewriting it as a `let` would stop compiling with the feature enabled. #[allow(clippy::infallible_destructuring_match)] fn parse_command_defaults_realm_metadata_paths() { - let mode = parse_command(["serve".to_owned(), "--test-mode".to_owned()]) - .expect("serve command parses with defaults"); + let mode = parse_command(["host".to_owned(), "--test-mode".to_owned()]) + .expect("host command parses with defaults"); let config = match mode { - BrokerMode::Serve(config) => config, + BrokerMode::Host(config) | BrokerMode::Guest(config) => config, #[cfg(feature = "layer1-bootstrap")] other => panic!("expected serve mode, got {other:?}"), }; @@ -13791,7 +12477,6 @@ mod tests { let processes = ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: vec![ VmProcessDag { workload_identity: None, @@ -13956,7 +12641,6 @@ mod tests { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -14170,6 +12854,8 @@ mod tests { #[cfg(not(feature = "layer1-bootstrap"))] fn test_server_config(root: &Path, manifest_path: &Path) -> ServerConfig { ServerConfig { + profile: BrokerProfile::Host, + authority_id: "test-host".to_owned(), socket_path: root.join("broker.sock"), audit_dir: root.join("audit"), audit_retention_days: 14, @@ -14178,7 +12864,6 @@ mod tests { realm_identity_path: root.join("realm-identity.json"), state_dir: root.join("state"), activation_helper_path: root.join("activation-helper"), - cutover_runner_path: root.join("cutover-runner"), d2bd_uid: 1000, d2bd_gid: Gid::current().as_raw(), store_sync_export_dir: root.join("observability").join("store-sync"), @@ -14256,110 +12941,6 @@ mod tests { } } - #[cfg(not(feature = "layer1-bootstrap"))] - fn guest_control_test_root(prefix: &str) -> tempfile::TempDir { - let base = crate::test_scratch_root().join("guest-control-tests"); - crate::sys::path_safe::ensure_dir(&base, 0o750, None, None) - .expect("create guest-control test root"); - tempfile::Builder::new() - .prefix(prefix) - .tempdir_in(base) - .expect("create guest-control test directory") - } - - #[cfg(not(feature = "layer1-bootstrap"))] - fn write_guest_control_token(state_dir: &Path, vm: &str, mode: u32) { - use std::os::unix::fs::PermissionsExt; - let dir = state_dir.join(format!("guest-control-{vm}")); - fs::create_dir_all(&dir).expect("create guest-control token dir"); - fs::set_permissions(&dir, fs::Permissions::from_mode(0o750)) - .expect("chmod guest-control token dir"); - let token = dir.join("token"); - if token.exists() { - fs::set_permissions(&token, fs::Permissions::from_mode(0o600)) - .expect("restore token write perms"); - fs::remove_file(&token).expect("remove old token"); - } - fs::write(&token, b"broker-test-token\n").expect("write token"); - fs::set_permissions(&token, fs::Permissions::from_mode(mode)).expect("chmod token"); - } - - #[cfg(not(feature = "layer1-bootstrap"))] - fn guest_control_sign_request( - role: d2b_contracts_broker::broker_wire::GuestControlProofRole, - ) -> d2b_contracts_broker::broker_wire::GuestControlSignRequest { - d2b_contracts_broker::broker_wire::GuestControlSignRequest { - vm_id: d2b_contracts::types::VmId::new("corp-vm"), - role, - protocol_version: d2b_contracts_control::guest_wire::GUEST_CONTROL_PROTOCOL_VERSION, - direction: d2b_contracts_broker::broker_wire::GuestControlDirection::HostToGuest, - purpose: d2b_contracts_broker::broker_wire::GuestControlAuthPurpose::GuestControlAuthV1, - guest_control_port: d2b_contracts_control::guest_auth::GUEST_CONTROL_AUTH_PORT, - peer_cid: Some(2), - host_nonce: vec![0x11; d2b_contracts_control::guest_auth::AUTH_NONCE_LEN], - guest_nonce: vec![0x22; d2b_contracts_control::guest_auth::AUTH_NONCE_LEN], - guest_boot_id: d2b_contracts_broker::broker_wire::GuestBootIdWire::new("boot-1"), - capabilities_hash: match role { - d2b_contracts_broker::broker_wire::GuestControlProofRole::HostProof => None, - d2b_contracts_broker::broker_wire::GuestControlProofRole::GuestProof => { - Some("caps-sha256".to_owned()) - } - }, - tracing_span_id: None, - } - } - - #[cfg(not(feature = "layer1-bootstrap"))] - #[test] - fn guest_control_sign_returns_only_fixed_tag() { - let root = guest_control_test_root("guest-control-sign-"); - let bundle = build_test_bundle(root.path()); - let config = test_server_config(root.path(), &bundle.bundle_path); - write_guest_control_token(&config.state_dir, "corp-vm", 0o440); - let response = handle_guest_control_sign( - guest_control_sign_request( - d2b_contracts_broker::broker_wire::GuestControlProofRole::HostProof, - ), - &config, - Some(&bundle.resolver), - ) - .expect("sign"); - assert_eq!( - response.tag.len(), - d2b_contracts_control::guest_auth::AUTH_TAG_LEN - ); - } - - #[cfg(not(feature = "layer1-bootstrap"))] - #[test] - fn guest_control_sign_rejects_role_confusion_and_unsafe_token() { - let root = guest_control_test_root("guest-control-sign-reject-"); - let bundle = build_test_bundle(root.path()); - let config = test_server_config(root.path(), &bundle.bundle_path); - write_guest_control_token(&config.state_dir, "corp-vm", 0o440); - - let mut bad = guest_control_sign_request( - d2b_contracts_broker::broker_wire::GuestControlProofRole::HostProof, - ); - bad.capabilities_hash = Some("caps-sha256".to_owned()); - assert!(matches!( - handle_guest_control_sign(bad, &config, Some(&bundle.resolver)), - Err(BrokerError::GuestControlSignRefused { .. }) - )); - - write_guest_control_token(&config.state_dir, "corp-vm", 0o666); - assert!(matches!( - handle_guest_control_sign( - guest_control_sign_request( - d2b_contracts_broker::broker_wire::GuestControlProofRole::HostProof - ), - &config, - Some(&bundle.resolver), - ), - Err(BrokerError::GuestControlSignRefused { .. }) - )); - } - #[cfg(not(feature = "layer1-bootstrap"))] #[test] fn executable_identity_accepts_trusted_symlink_paths() { @@ -15487,6 +14068,7 @@ mod tests { expected_start_time_ticks: 123456, resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: Some(TracingSpanId::new("span-pidfd")), }), "OpenPidfd", @@ -15516,6 +14098,7 @@ mod tests { expected_start_time_ticks: None, resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: Some(TracingSpanId::new("span-signal")), }), "SignalRunner", @@ -15540,12 +14123,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -15991,12 +14576,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -16697,12 +15284,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -16836,12 +15425,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -16922,6 +15513,7 @@ mod tests { expected_start_time_ticks: None, resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: None, }); let audit_context = DispatchAuditContext::from_request(&request, 5151, &caller_role) @@ -17147,26 +15739,24 @@ mod tests { .expect("send broker request"); handle_connection(server, &config, &log, None, None, &limiter) .expect("handle refused peer"); - let response = crate::protocol::recv_json_frame::(client.as_raw_fd()) - .expect("receive refusal response") - .expect("broker sends typed refusal"); - let BrokerResponse::Error(error) = response else { - panic!("expected peer credential refusal for {operation}"); - }; - assert_eq!(error.kind, "Broker.PeerCredentialRefused"); - assert_eq!(error.operation, operation); - let rendered = format!("{} {}", error.message, error.action); - assert!(!rendered.contains(&actual_uid.to_string()), "{rendered}"); - assert!(!rendered.contains("1-2.3"), "{rendered}"); - assert!(!rendered.contains("/"), "{rendered}"); + let response = crate::protocol::recv_json_frame::(client.as_raw_fd()); + assert!( + matches!( + response, + Err(ref error) if error.kind() == io::ErrorKind::ConnectionReset + ) || matches!(response, Ok(None)), + "unauthenticated peers must be closed before request decoding: {response:?}" + ); } let audit = fs::read_to_string(log.current_daily_path()).expect("read audit log"); - for operation in operations { - assert!(audit.contains(&format!(r#""op":"{operation}""#)), "{audit}"); + for _operation in operations { + assert!(audit.contains(r#""op":"PeerAuthentication""#), "{audit}"); } assert_eq!( - audit.matches(r#""disposition":"peer-refused""#).count(), + audit + .matches(r#""disposition":"peer-refused-before-decode""#) + .count(), 4, "{audit}" ); @@ -18983,21 +17573,5 @@ mod tests { assert!(drained.iter().any(|n| n.runner_id == "overflow-256")); } - #[cfg(not(feature = "layer1-bootstrap"))] - #[test] - fn cutover_admission_requires_both_artifact_bindings() { - let request = d2b_contracts_broker::broker_wire::CutoverAdmissionRequest { - system_artifact_id: None, - source_system_artifact_id: None, - }; - assert!(!artifact_binding_shape_is_valid( - &request, - d2b_contracts_broker::broker_wire::CutoverEffectAuthority::Cutover, - )); - assert!(artifact_binding_shape_is_valid( - &request, - d2b_contracts_broker::broker_wire::CutoverEffectAuthority::ResetZone, - )); - } } } diff --git a/packages/d2b-priv-broker/src/seccomp_compile_tests.rs b/packages/d2b-broker/src/seccomp_compile_tests.rs similarity index 99% rename from packages/d2b-priv-broker/src/seccomp_compile_tests.rs rename to packages/d2b-broker/src/seccomp_compile_tests.rs index 2e749a04b9..18d2b7d401 100644 --- a/packages/d2b-priv-broker/src/seccomp_compile_tests.rs +++ b/packages/d2b-broker/src/seccomp_compile_tests.rs @@ -207,7 +207,7 @@ fn all_known_policy_refs_compile_to_some() { "w1-virtiofsd", "w1-host-reconcile", "w1-store-virtiofs-preflight", - "w1-guest-control-health", + "w1-component-session-health", "w1-swtpm", "w1-gpu", "w1-gpu-render-node", diff --git a/packages/d2b-priv-broker/src/sys.rs b/packages/d2b-broker/src/sys.rs similarity index 95% rename from packages/d2b-priv-broker/src/sys.rs rename to packages/d2b-broker/src/sys.rs index 2350f1e583..893f4dd52e 100644 --- a/packages/d2b-priv-broker/src/sys.rs +++ b/packages/d2b-broker/src/sys.rs @@ -1519,8 +1519,7 @@ pub mod pidfd_sys { use std::ffi::CString; use std::io; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}; - use std::os::unix::ffi::OsStrExt; - use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt}; + use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::Path; use d2b_core::minijail_profile::{MountPolicy, NamespaceSet}; @@ -1701,92 +1700,6 @@ pub mod pidfd_sys { }) } - /// Spawn the one-shot cutover runner into the host cgroup root. - /// - /// This is deliberately separate from `SpawnRunner`: the runner is a - /// root-owned, operation-scoped process outside `d2b.slice`, receives - /// exactly one bootstrap fd, and has no VM runner profile or cgroup leaf. - /// If atomic `CLONE_INTO_CGROUP` is unavailable, the child is killed and - /// the operation fails closed rather than falling back into `d2b.slice`. - #[allow(unsafe_code)] - pub fn spawn_cutover_runner( - binary_path: &Path, - state_dir: &Path, - socket_dir: &Path, - bootstrap_fd: RawFd, - ) -> io::Result { - if !binary_path.is_absolute() - || !binary_path - .to_str() - .is_some_and(|path| path.starts_with("/nix/store/")) - { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "cutover runner must be a trusted store path", - )); - } - let fd_flags = unsafe { libc::fcntl(bootstrap_fd, libc::F_GETFD) }; - if fd_flags < 0 || fd_flags & libc::FD_CLOEXEC == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "cutover bootstrap fd must be close-on-exec", - )); - } - let binary = CString::new(binary_path.as_os_str().as_bytes()) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "runner path contains NUL"))?; - let argv = - [CString::new("d2b-cutover-runner").expect("static cutover runner argv has no NUL")]; - let env = [ - CString::new(format!("D2B_CUTOVER_BOOTSTRAP_FD={bootstrap_fd}")) - .expect("fd env has no NUL"), - CString::new(format!("D2B_CUTOVER_STATE_DIR={}", state_dir.display())).map_err( - |_| io::Error::new(io::ErrorKind::InvalidInput, "state path contains NUL"), - )?, - CString::new(format!("D2B_CUTOVER_SOCKET_DIR={}", socket_dir.display())).map_err( - |_| io::Error::new(io::ErrorKind::InvalidInput, "socket path contains NUL"), - )?, - ]; - let cgroup_root = std::fs::OpenOptions::new() - .read(true) - .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW) - .open("/sys/fs/cgroup")?; - let mut argv_ptrs = argv.iter().map(|arg| arg.as_ptr()).collect::>(); - argv_ptrs.push(std::ptr::null()); - let mut env_ptrs = env.iter().map(|entry| entry.as_ptr()).collect::>(); - env_ptrs.push(std::ptr::null()); - let binary_ptr = binary.as_ptr(); - let outcome = clone3_pidfd_or_fork_fallback_with_cgroup( - 0, - Some(cgroup_root.as_raw_fd()), - move |placed_in_cgroup| { - if !placed_in_cgroup { - return 75; - } - // Keep the broker's received fd CLOEXEC. Only this - // single-threaded child clears it immediately before exec; - // the runner re-secures it before reading the bootstrap. - if unsafe { libc::fcntl(bootstrap_fd, libc::F_SETFD, 0) } < 0 { - return 76; - } - unsafe { - libc::execve(binary_ptr, argv_ptrs.as_ptr(), env_ptrs.as_ptr()); - } - 73 - }, - )?; - if outcome.used_fork_fallback { - unsafe { - libc::kill(outcome.pid, libc::SIGKILL); - } - let _ = reap_cutover_runner_error_child(outcome.pid); - return Err(io::Error::new( - io::ErrorKind::Unsupported, - "atomic out-of-band cgroup placement unavailable", - )); - } - Ok(outcome) - } - /// Duplicate an already-owned fd while preserving `FD_CLOEXEC`. #[allow(unsafe_code)] pub fn dup_fd_cloexec(fd: RawFd) -> io::Result { @@ -1883,32 +1796,6 @@ pub mod pidfd_sys { } } - /// Synchronously reap the cutover runner child when atomic placement - /// fails. This stays separate from SpawnRunner's named reaper contract. - #[allow(unsafe_code)] - fn reap_cutover_runner_error_child(child_pid: i32) -> io::Result<()> { - if child_pid <= 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "invalid cutover runner pid", - )); - } - let mut status = 0; - loop { - let rc = unsafe { libc::waitpid(child_pid, &mut status, 0) }; - if rc == child_pid { - return Ok(()); - } - if rc < 0 { - let error = io::Error::last_os_error(); - if error.raw_os_error() == Some(libc::EINTR) { - continue; - } - return Err(error); - } - } - } - /// Run `setfacl -m /proc/self/fd/` in a forked /// child while keeping the target fd CLOEXEC in the broker parent. /// @@ -2179,6 +2066,10 @@ pub mod pidfd_sys { /// dropping credentials. Used only for qemu-media runners whose /// trusted argv requests QEMU `mem-lock=on`. pub memlock_limit_bytes: Option, + /// Optional broker-controlled stdin envelope for an activation + /// runner. The broker creates the pipe and writes these validated + /// bytes only after the child has been spawned. + pub activation_stdin: Option>, } /// Well-known fd number for the pre-opened render node. @@ -2953,6 +2844,8 @@ pub mod pidfd_sys { const CHILD_EXIT_PREOPEN_DUP2: libc::c_int = 76; /// setrlimit(RLIMIT_MEMLOCK) failed for a runner that requested it. const CHILD_EXIT_MEMLOCK_RLIMIT: libc::c_int = 77; + /// dup2 of the broker-controlled activation stdin pipe failed. + const CHILD_EXIT_ACTIVATION_STDIN: libc::c_int = 78; /// Spawn a per-role runner with namespace / seccomp / capability /// setup plus `setgroups` + `setgid` + `setuid` + `execve` in a @@ -2972,6 +2865,14 @@ pub mod pidfd_sys { supplementary_groups: Vec, isolation: RunnerIsolationSpec, ) -> io::Result { + if isolation.activation_stdin.as_ref().is_some_and(|payload| { + payload.len() > d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + }) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "SpawnRunner: activation stdin exceeds bounded envelope", + )); + } // NamespaceSet.user is ALLOWED when // RunnerIsolationSpec.user_namespace provides the uid_map/gid_map // values. Caller must set both for the child to be fake-root @@ -3092,6 +2993,26 @@ pub mod pidfd_sys { .map(|p| p.write_fd.as_raw_fd()) .unwrap_or(-1); + // Activation runners receive a broker-created stdin pipe. The child + // owns only the read end; the parent writes the already validated + // typed envelope after clone and closes its end to delimit input. + let (activation_stdin_read_owner, activation_stdin_write_fd) = + match isolation.activation_stdin.as_ref() { + Some(_) => { + let pipe = make_sync_pipe()?; + (Some(pipe.read_fd), Some(pipe.write_fd)) + } + None => (None, None), + }; + let activation_stdin_read_fd = activation_stdin_read_owner + .as_ref() + .map(|fd| fd.as_raw_fd()) + .unwrap_or(-1); + let activation_stdin_write_raw_fd = activation_stdin_write_fd + .as_ref() + .map(|fd| fd.as_raw_fd()) + .unwrap_or(-1); + // When a user namespace is in play, the child must transition // to in-NS UID 0 (the mapped root), NOT to the host stable // principal UID. Calling setuid() inside the new user @@ -3124,6 +3045,25 @@ pub mod pidfd_sys { extra_clone_flags, cgroup_dir_raw_fd, move |_| unsafe { + // Keep the parent's read-end owner live through clone/fork; + // the child inherits the raw descriptor captured below. + let _activation_stdin_read_owner = &activation_stdin_read_owner; + if activation_stdin_read_fd >= 0 { + if activation_stdin_write_raw_fd >= 0 { + libc::close(activation_stdin_write_raw_fd); + } + if activation_stdin_read_fd != libc::STDIN_FILENO { + if libc::dup2(activation_stdin_read_fd, libc::STDIN_FILENO) < 0 { + let m = b"DEBUG: dup2 activation stdin failed\n"; + libc::write(2, m.as_ptr() as *const _, m.len()); + libc::_exit(CHILD_EXIT_ACTIVATION_STDIN); + } + libc::close(activation_stdin_read_fd); + } + // dup2 does not copy CLOEXEC; clear it for the + // read-end-already-at-stdin case. + libc::fcntl(libc::STDIN_FILENO, libc::F_SETFD, 0); + } // If we're in a user NS, FIRST close the inherited write end // of the sync pipe so the parent's death is observable as EOF. // THEN block on the read end until the parent has written @@ -3443,6 +3383,37 @@ pub mod pidfd_sys { } } + if let Some(payload) = isolation.activation_stdin.as_deref() { + let write_fd = activation_stdin_write_fd.ok_or_else(|| { + io::Error::new( + io::ErrorKind::BrokenPipe, + "activation stdin pipe was not created", + ) + })?; + let mut offset = 0; + while offset < payload.len() { + match rustix::io::write(&write_fd, &payload[offset..]) { + Ok(0) => { + let _ = pidfd_send_signal(outcome.pidfd.as_fd(), libc::SIGKILL); + let _ = reap_spawn_runner_error_child(outcome.pid); + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "activation stdin pipe closed before payload delivery", + )); + } + Ok(written) => offset += written, + Err(error) if error.raw_os_error() == libc::EINTR => {} + Err(error) => { + let _ = pidfd_send_signal(outcome.pidfd.as_fd(), libc::SIGKILL); + let _ = reap_spawn_runner_error_child(outcome.pid); + return Err(io::Error::from_raw_os_error(error.raw_os_error())); + } + } + } + // Dropping the write end sends EOF to the helper. + drop(write_fd); + } + Ok(outcome) } @@ -3514,6 +3485,7 @@ pub mod pidfd_sys { #[cfg(test)] mod tests { + use std::ffi::CString; use std::fs; use std::os::fd::{AsRawFd, OwnedFd}; use std::os::unix::fs::{PermissionsExt, symlink}; @@ -3522,6 +3494,8 @@ mod tests { use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use nix::libc; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; + use nix::sys::wait::{WaitStatus, waitpid}; + use nix::unistd::Pid; use tempfile::tempdir; fn test_namespaces(pid: bool) -> NamespaceSet { @@ -3691,7 +3665,6 @@ mod tests { // requires root and is tested by integration tests in live_handlers.rs). use super::pidfd_sys::{RunnerIsolationSpec, UserNamespaceSpec, clone3_spawn_runner}; - use std::ffi::CString; fn empty_mount_policy() -> MountPolicy { MountPolicy { @@ -3723,6 +3696,7 @@ mod tests { umask: None, pre_opened_device_fds: Vec::new(), memlock_limit_bytes: None, + activation_stdin: None, } } @@ -3773,6 +3747,67 @@ mod tests { assert_eq!(iso.umask, Some(7)); } + #[test] + fn activation_stdin_delivery_is_broker_controlled() { + let shell_path = ["/bin/sh", "/usr/bin/sh"] + .iter() + .find(|path| std::path::Path::new(path).exists()) + .copied() + .expect("could not find a shell binary"); + let current_uid = nix::unistd::Uid::current().as_raw(); + let current_gid = nix::unistd::Gid::current().as_raw(); + if current_uid != 0 { + eprintln!("SKIP: activation stdin child test requires broker credentials"); + return; + } + let mut isolation = isolation_with_user_namespace(None); + isolation.activation_stdin = Some(b"expected\n".to_vec()); + let binary = CString::new(shell_path).expect("shell path"); + let supplementary_groups = nix::unistd::getgroups() + .expect("read supplementary groups") + .into_iter() + .map(|group| group.as_raw()) + .collect(); + let outcome = clone3_spawn_runner( + binary.clone(), + vec![ + binary, + CString::new("-c").expect("shell option"), + CString::new(r#"read -r value && [ "$value" = expected ]"#) + .expect("shell script"), + ], + Vec::new(), + current_uid, + current_gid, + supplementary_groups, + isolation, + ) + .expect("activation stdin runner should spawn"); + let status = waitpid(Pid::from_raw(outcome.pid), None).expect("wait for runner"); + assert!(matches!(status, WaitStatus::Exited(_, 0)), "status: {status:?}"); + } + + #[test] + fn activation_stdin_bound_is_enforced_before_spawn() { + let mut isolation = isolation_with_user_namespace(None); + isolation.activation_stdin = Some(vec![ + 0; + d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + 1 + ]); + let binary = CString::new("/bin/true").expect("true path"); + let error = clone3_spawn_runner( + binary, + Vec::new(), + Vec::new(), + nix::unistd::Uid::current().as_raw(), + nix::unistd::Gid::current().as_raw(), + Vec::new(), + isolation, + ) + .expect_err("oversized activation stdin must be refused"); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + } + #[test] fn child_spawn_path_does_not_log_full_argv_or_env_to_global_file() { let source = include_str!("sys.rs"); @@ -3794,29 +3829,6 @@ mod tests { ); } - #[test] - fn cutover_runner_fd_contract_clears_only_in_child_before_exec() { - let source = include_str!("sys.rs"); - assert!( - source.contains("fd_flags < 0 || fd_flags & libc::FD_CLOEXEC == 0"), - "broker must require the received bootstrap fd to be CLOEXEC" - ); - assert!( - source.contains("D2B_CUTOVER_BOOTSTRAP_FD={bootstrap_fd}"), - "broker must identify the inherited bootstrap fd for the runner" - ); - let clear = source - .find("if unsafe { libc::fcntl(bootstrap_fd, libc::F_SETFD, 0) } < 0") - .expect("child must clear CLOEXEC before exec"); - let exec = source - .find("libc::execve(binary_ptr, argv_ptrs.as_ptr(), env_ptrs.as_ptr())") - .expect("child must exec the trusted runner"); - assert!( - clear < exec, - "the child must clear CLOEXEC immediately before exec" - ); - } - fn owned_pipe_for_test() -> (OwnedFd, OwnedFd) { nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC).expect("pipe2 failed") } @@ -3911,8 +3923,8 @@ mod tests { ); assert_eq!( source.matches(reap_error_child).count(), - 4, - "fallback cgroup, user-NS map, sync-write error, and sync-write short-write paths must each reap before returning Err" + 6, + "fallback cgroup, user-NS map, sync-write error, sync-write short-write, activation stdin write error, and activation stdin short-write paths must each reap before returning Err" ); assert!( !source.contains(removed_child_helper), @@ -4075,6 +4087,7 @@ mod tests { umask: None, pre_opened_device_fds: Vec::new(), memlock_limit_bytes: None, + activation_stdin: None, }; let bin = CString::new(true_path).unwrap(); diff --git a/packages/d2b-priv-broker/tests/bridge_lifecycle.rs b/packages/d2b-broker/tests/bridge_lifecycle.rs similarity index 99% rename from packages/d2b-priv-broker/tests/bridge_lifecycle.rs rename to packages/d2b-broker/tests/bridge_lifecycle.rs index 18d37ddc9c..abe67c8f1a 100644 --- a/packages/d2b-priv-broker/tests/bridge_lifecycle.rs +++ b/packages/d2b-broker/tests/bridge_lifecycle.rs @@ -2,7 +2,7 @@ use std::cell::{Cell, RefCell}; use d2b_contracts_resource::v3::IfName; use d2b_core::bundle_resolver::ResolvedBridgeIntent; -use d2b_priv_broker::ops::network::{ +use d2b_broker::ops::network::{ BridgeBackend, BridgeReadback, NetworkOpError, bridge_intent_digest, create_bridge, delete_bridge, }; diff --git a/packages/d2b-priv-broker/tests/broker_export_audit.rs b/packages/d2b-broker/tests/broker_export_audit.rs similarity index 100% rename from packages/d2b-priv-broker/tests/broker_export_audit.rs rename to packages/d2b-broker/tests/broker_export_audit.rs diff --git a/packages/d2b-priv-broker/tests/broker_protocol_compatibility.rs b/packages/d2b-broker/tests/broker_protocol_compatibility.rs similarity index 100% rename from packages/d2b-priv-broker/tests/broker_protocol_compatibility.rs rename to packages/d2b-broker/tests/broker_protocol_compatibility.rs diff --git a/packages/d2b-priv-broker/tests/broker_socket_acl.rs b/packages/d2b-broker/tests/broker_socket_acl.rs similarity index 100% rename from packages/d2b-priv-broker/tests/broker_socket_acl.rs rename to packages/d2b-broker/tests/broker_socket_acl.rs diff --git a/packages/d2b-priv-broker/tests/bundle_tampered_broker.rs b/packages/d2b-broker/tests/bundle_tampered_broker.rs similarity index 98% rename from packages/d2b-priv-broker/tests/bundle_tampered_broker.rs rename to packages/d2b-broker/tests/bundle_tampered_broker.rs index 1c13e26ad2..95e40adc23 100644 --- a/packages/d2b-priv-broker/tests/bundle_tampered_broker.rs +++ b/packages/d2b-broker/tests/bundle_tampered_broker.rs @@ -6,7 +6,7 @@ //! `"Broker.BundleResolverUnavailable"` or any other kind). //! //! The test uses the public `probe_bundle_load_response` helper exported -//! from `d2b_priv_broker::runtime`, which exercises the same +//! from `d2b_broker::runtime`, which exercises the same //! `try_load_resolver` → `BundleSlot::Tampered` → `BrokerError::BundleTampered` //! → `into_response()` pipeline as the live `serve` loop. @@ -14,7 +14,7 @@ mod broker_tampered { use d2b_contracts_broker::broker_wire::BrokerResponse; use d2b_core::bundle_resolver::BundleVerifyPolicy; - use d2b_priv_broker::runtime::{ + use d2b_broker::runtime::{ probe_bundle_load_response, probe_bundle_load_response_with_policy, }; use std::fs; diff --git a/packages/d2b-priv-broker/tests/common/mod.rs b/packages/d2b-broker/tests/common/mod.rs similarity index 88% rename from packages/d2b-priv-broker/tests/common/mod.rs rename to packages/d2b-broker/tests/common/mod.rs index 37cc82883c..ee9e19b180 100644 --- a/packages/d2b-priv-broker/tests/common/mod.rs +++ b/packages/d2b-broker/tests/common/mod.rs @@ -10,7 +10,7 @@ use std::time::Duration; use tempfile::{Builder, TempDir}; -const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-priv-broker"); +const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-broker"); const O_APPEND: u32 = 0o2000; pub const D2BD_UID: u32 = 4242; @@ -52,34 +52,53 @@ pub struct TestBroker { impl TestBroker { pub fn spawn(prefix: &str) -> Self { + Self::spawn_profile(prefix, "host-test", "host", D2BD_UID) + } + + pub fn spawn_profile(prefix: &str, authority_id: &str, profile: &str, d2bd_uid: u32) -> Self { let scratch = Scratch::new(prefix); let run_dir = scratch.path().join("run/d2b"); let audit_dir = scratch.path().join("var/lib/d2b/audit"); + let state_dir = scratch.path().join("var/lib/d2b/state"); + let realm_controllers_path = scratch.path().join("realm-controllers.json"); + let realm_identity_path = scratch.path().join("realm-identity.json"); fs::create_dir_all(&run_dir).expect("create broker run dir"); fs::create_dir_all(&audit_dir).expect("create broker audit dir"); + fs::create_dir_all(&state_dir).expect("create broker state dir"); let socket_path = run_dir.join("priv.sock"); + let bundle_path = scratch.path().join("unused-bundle.json"); let server_log_path = scratch.path().join("server.log"); let server_log = File::create(&server_log_path).expect("create broker server log"); let server_log_err = server_log.try_clone().expect("clone broker server log"); let current_gid = nix::unistd::Gid::current().as_raw(); let child = Command::new(BROKER_BIN) - .arg("serve") + .arg(profile) + .arg("--authority-id") + .arg(authority_id) .arg("--socket-path") .arg(&socket_path) .arg("--audit-dir") .arg(&audit_dir) + .arg("--bundle-path") + .arg(&bundle_path) + .arg("--realm-controllers-path") + .arg(&realm_controllers_path) + .arg("--realm-identity-path") + .arg(&realm_identity_path) .arg("--d2bd-uid") - .arg(D2BD_UID.to_string()) + .arg(d2bd_uid.to_string()) .arg("--d2bd-gid") .arg(current_gid.to_string()) + .arg("--state-dir") + .arg(&state_dir) .arg("--test-mode") .env("RUST_LOG", "off") .stdout(Stdio::from(server_log)) .stderr(Stdio::from(server_log_err)) .spawn() - .expect("spawn d2b-priv-broker serve"); + .expect("spawn d2b-broker profile"); let broker = Self { scratch, @@ -87,7 +106,7 @@ impl TestBroker { socket_path, audit_dir, server_log_path, - d2bd_uid: D2BD_UID, + d2bd_uid, }; broker.wait_for_socket(); broker @@ -113,6 +132,10 @@ impl TestBroker { self.d2bd_uid } + pub fn socket_path(&self) -> &Path { + &self.socket_path + } + pub fn caller_gid(&self) -> u32 { nix::unistd::Gid::current().as_raw() } diff --git a/packages/d2b-broker/tests/guest_profile.rs b/packages/d2b-broker/tests/guest_profile.rs new file mode 100644 index 0000000000..d28a3326b1 --- /dev/null +++ b/packages/d2b-broker/tests/guest_profile.rs @@ -0,0 +1,147 @@ +use d2b_contracts_broker::broker_wire::BrokerProfile; + +#[path = "common/mod.rs"] +#[cfg(not(feature = "layer1-bootstrap"))] +mod common; + +#[cfg(not(feature = "layer1-bootstrap"))] +use std::os::fd::AsRawFd; + +#[cfg(not(feature = "layer1-bootstrap"))] +use common::{D2BD_UID, TestBroker}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts::types::{BundleOpId, RoleId, ScopeId, VmId}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts_broker::broker_wire::{ + ApplyNftablesRequest, BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, + RunnerRole, SpawnRunnerRequest, +}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts_resource::v3::ResourceRef; + +#[test] +fn guest_profile_admits_only_local_process_effects() { + for operation in [ + "Hello", + "ValidateBundle", + "ExportBrokerAudit", + "SpawnRunner", + "OpenPidfd", + "ObserveRunner", + "SignalRunner", + "DeregisterRunnerPidfd", + "StartSystemdUnit", + "StopSystemdUnit", + ] { + assert!( + BrokerProfile::Guest.allows_operation(operation), + "guest profile should admit declared local effect {operation}" + ); + } +} + +#[test] +fn guest_profile_rejects_every_host_only_effect_class() { + for operation in [ + "ApplyNftables", + "ApplyRoute", + "ApplySysctl", + "CreateBridge", + "OpenKvm", + "OpenHidrawSecurityKey", + "OpenZoneStore", + "StoreSync", + "RunHostInstall", + "RunMigrate", + "UsbipBind", + "SecurityKeyOpenDevice", + ] { + assert!( + !BrokerProfile::Guest.allows_operation(operation), + "guest profile must reject host-only operation {operation}" + ); + } +} + +#[test] +#[cfg(not(feature = "layer1-bootstrap"))] +fn guest_binary_rejects_host_effects_before_bundle_mutation() { + let broker = TestBroker::spawn_profile("guest-profile-", "guest-test", "guest", D2BD_UID); + let client = connect_seqpacket(broker.socket_path()).expect("connect guest broker"); + let envelope = BrokerRequestEnvelope { + request: BrokerRequest::ApplyNftables(ApplyNftablesRequest { + bundle_nft_intent_ref: BundleOpId::new("nft:host-only"), + scope_id: ScopeId::new("env:work"), + desired_hash: None, + destroy: false, + tracing_span_id: None, + }), + caller_role: BrokerCallerRole::AdminUid { uid: D2BD_UID }, + test_peer_uid: Some(D2BD_UID), + audit_join: None, + }; + send_json_frame(client.as_raw_fd(), &envelope).expect("send host-only request"); + let response: BrokerResponse = recv_json_frame(client.as_raw_fd()) + .expect("receive guest profile response") + .expect("guest broker response"); + + let BrokerResponse::Error(error) = response else { + panic!("guest profile must return a typed denial"); + }; + assert_eq!(error.kind, "Broker.ProfileOperationDenied"); + assert_eq!(error.operation, "ApplyNftables"); + assert!(broker.audit_contents().contains("profile-operation-denied")); +} + +#[test] +#[cfg(not(feature = "layer1-bootstrap"))] +fn guest_binary_rejects_spawn_runner_before_bundle_mutation() { + let broker = + TestBroker::spawn_profile("guest-runner-profile-", "guest-test", "guest", D2BD_UID); + + for execution_ref in ["Guest/guest-vm", "Host/host"] { + let client = connect_seqpacket(broker.socket_path()).expect("connect guest broker"); + let envelope = BrokerRequestEnvelope { + request: BrokerRequest::SpawnRunner(SpawnRunnerRequest { + vm_id: VmId::new("guest-vm"), + role_id: RoleId::new("cloud-hypervisor"), + resource_ref: None, + resource_uid: None, + bundle_content_identity: None, + provider_identity: None, + template_identity: None, + generation: None, + activation_input: None, + sandbox_plan: None, + role: RunnerRole::CloudHypervisor, + bundle_runner_intent_ref: BundleOpId::new("runner:test"), + execution_ref: Some( + ResourceRef::parse(execution_ref).expect("valid execution ref"), + ), + execution_domain: None, + user_ref: None, + guest_execution: None, + runtime_allocations: Vec::new(), + tracing_span_id: None, + workload_identity: None, + }), + caller_role: BrokerCallerRole::AdminUid { uid: D2BD_UID }, + test_peer_uid: Some(D2BD_UID), + audit_join: None, + }; + send_json_frame(client.as_raw_fd(), &envelope).expect("send SpawnRunner request"); + let response: BrokerResponse = recv_json_frame(client.as_raw_fd()) + .expect("receive guest profile response") + .expect("guest broker response"); + + let BrokerResponse::Error(error) = response else { + panic!("guest broker must refuse SpawnRunner before bundle dispatch"); + }; + assert_eq!(error.kind, "Broker.ProfileOperationDenied"); + assert_eq!(error.operation, "SpawnRunner"); + } + + assert!(broker.audit_contents().contains("profile-operation-denied")); +} diff --git a/packages/d2b-broker/tests/host_profile.rs b/packages/d2b-broker/tests/host_profile.rs new file mode 100644 index 0000000000..b287004ceb --- /dev/null +++ b/packages/d2b-broker/tests/host_profile.rs @@ -0,0 +1,30 @@ +use d2b_contracts_broker::broker_wire::BrokerProfile; + +#[test] +fn host_profile_keeps_the_complete_closed_operation_catalog() { + let operations = BrokerProfile::Host.operations(); + + for operation in [ + "ApplyNftables", + "OpenZoneStore", + "SpawnRunner", + "ApplyHostGenerationHandoff", + "ExportBrokerAudit", + "ValidateBundle", + ] { + assert!( + operations.contains(&operation), + "host profile lost the existing operation {operation}" + ); + assert!( + BrokerProfile::Host.allows_operation(operation), + "host profile must admit {operation}" + ); + } +} + +#[test] +fn host_profile_is_not_an_open_ended_default() { + assert!(!BrokerProfile::Host.allows_operation("SelectProfile")); + assert!(!BrokerProfile::Host.allows_operation("UnknownFutureOperation")); +} diff --git a/packages/d2b-priv-broker/tests/kernel_surface.rs b/packages/d2b-broker/tests/kernel_surface.rs similarity index 99% rename from packages/d2b-priv-broker/tests/kernel_surface.rs rename to packages/d2b-broker/tests/kernel_surface.rs index 2c25969d67..1ffd16e7d7 100644 --- a/packages/d2b-priv-broker/tests/kernel_surface.rs +++ b/packages/d2b-broker/tests/kernel_surface.rs @@ -3,7 +3,7 @@ use std::io; use std::os::unix::fs::PermissionsExt; use std::process::Command; -use d2b_priv_broker::sys::path_safe; +use d2b_broker::sys::path_safe; use tempfile::TempDir; const OPEN_DIR_XDEV_HELPER_ENV: &str = "D2B_OPEN_DIR_XDEV_HELPER"; diff --git a/packages/d2b-priv-broker/tests/persistent_tap_lifecycle.rs b/packages/d2b-broker/tests/persistent_tap_lifecycle.rs similarity index 99% rename from packages/d2b-priv-broker/tests/persistent_tap_lifecycle.rs rename to packages/d2b-broker/tests/persistent_tap_lifecycle.rs index 6229dd84a1..f55fd932b7 100644 --- a/packages/d2b-priv-broker/tests/persistent_tap_lifecycle.rs +++ b/packages/d2b-broker/tests/persistent_tap_lifecycle.rs @@ -6,7 +6,7 @@ use d2b_contracts_resource::v3::{ ResourceGeneration, ResourceUid, }; -use d2b_priv_broker::ops::{ +use d2b_broker::ops::{ audit_op::OperationFields, network::{ NetworkOpError, PersistentTapBackend, PersistentTapRealization, attachment_digest, diff --git a/packages/d2b-priv-broker/tests/pidfd_handoff_scm_rights.rs b/packages/d2b-broker/tests/pidfd_handoff_scm_rights.rs similarity index 89% rename from packages/d2b-priv-broker/tests/pidfd_handoff_scm_rights.rs rename to packages/d2b-broker/tests/pidfd_handoff_scm_rights.rs index 69db4c81f6..acde7bd27f 100644 --- a/packages/d2b-priv-broker/tests/pidfd_handoff_scm_rights.rs +++ b/packages/d2b-broker/tests/pidfd_handoff_scm_rights.rs @@ -1,9 +1,9 @@ //! Integration: pidfd handoff over `SCM_RIGHTS`. //! -//! Drives the broker's existing [`d2b_priv_broker::fd_passing`] +//! Drives the broker's existing [`d2b_broker::fd_passing`] //! helpers end-to-end against a real `socketpair(SOCK_SEQPACKET)`, //! using a pidfd-shaped stand-in fd from -//! [`d2b_priv_broker::ops::pidfd::test_harness`]. +//! [`d2b_broker::ops::pidfd::test_harness`]. //! //! Assertions: //! @@ -20,9 +20,9 @@ use std::os::fd::AsRawFd; -use d2b_priv_broker::fd_passing::{recv_fds, send_fds}; -use d2b_priv_broker::ops::pidfd::test_harness::FakePidfdSpawner; -use d2b_priv_broker::ops::pidfd::{PidfdPayload, PidfdSpawner, assert_cloexec}; +use d2b_broker::fd_passing::{recv_fds, send_fds}; +use d2b_broker::ops::pidfd::test_harness::FakePidfdSpawner; +use d2b_broker::ops::pidfd::{PidfdPayload, PidfdSpawner, assert_cloexec}; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; @@ -82,7 +82,7 @@ fn scm_rights_transports_pidfd_with_cloexec_preserved() { #[test] fn reconciliation_refuses_start_time_drift() { - use d2b_priv_broker::ops::pidfd::StartTime; + use d2b_broker::ops::pidfd::StartTime; let s = FakePidfdSpawner::default(); let err = s .reconcile_drift(4242, StartTime(1_000_000), StartTime(2_000_000)) diff --git a/packages/d2b-priv-broker/tests/pidfd_real_spawner.rs b/packages/d2b-broker/tests/pidfd_real_spawner.rs similarity index 98% rename from packages/d2b-priv-broker/tests/pidfd_real_spawner.rs rename to packages/d2b-broker/tests/pidfd_real_spawner.rs index ff64f4d55d..a4428f7eb2 100644 --- a/packages/d2b-priv-broker/tests/pidfd_real_spawner.rs +++ b/packages/d2b-broker/tests/pidfd_real_spawner.rs @@ -14,7 +14,7 @@ use std::os::fd::AsRawFd; use std::path::PathBuf; -use d2b_priv_broker::ops::pidfd::{ +use d2b_broker::ops::pidfd::{ PidfdMethod, PidfdOpError, PidfdPayload, PidfdSpawner, RealPidfdSpawner, StartTime, }; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; diff --git a/packages/d2b-broker/tests/profile_separation.rs b/packages/d2b-broker/tests/profile_separation.rs new file mode 100644 index 0000000000..0df8135413 --- /dev/null +++ b/packages/d2b-broker/tests/profile_separation.rs @@ -0,0 +1,146 @@ +use d2b_broker::runtime::{BrokerMode, parse_command}; + +#[path = "common/mod.rs"] +#[cfg(not(feature = "layer1-bootstrap"))] +mod common; + +#[cfg(not(feature = "layer1-bootstrap"))] +use std::os::fd::AsRawFd; + +#[cfg(not(feature = "layer1-bootstrap"))] +use common::{D2BD_UID, TestBroker}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts_broker::broker_wire::{ + BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, HelloRequest, +}; + +fn parse(args: &[&str]) -> BrokerMode { + parse_command(args.iter().map(|arg| (*arg).to_owned())).expect("profile command should parse") +} + +#[test] +fn profile_is_selected_at_process_start() { + let host = parse(&["host", "--test-mode"]); + let guest = parse(&["guest", "--test-mode"]); + + let (host_config, guest_config) = match (host, guest) { + (BrokerMode::Host(host), BrokerMode::Guest(guest)) => (host, guest), + other => panic!("unexpected broker modes: {other:?}"), + }; + + assert_eq!(host_config.profile.as_str(), "host"); + assert_eq!(guest_config.profile.as_str(), "guest"); + assert_ne!(host_config.profile, guest_config.profile); +} + +#[test] +fn each_profile_requires_its_own_instance_bindings() { + let host = parse(&[ + "host", + "--test-mode", + "--authority-id", + "host-authority", + "--d2bd-uid", + "1000", + "--socket-path", + "/run/d2b/host-broker.sock", + "--state-dir", + "/var/lib/d2b/host-broker", + "--audit-dir", + "/var/lib/d2b/host-audit", + ]); + let guest = parse(&[ + "guest", + "--test-mode", + "--authority-id", + "guest-authority", + "--d2bd-uid", + "1001", + "--socket-path", + "/run/d2b/guest-broker.sock", + "--state-dir", + "/var/lib/d2b/guest-broker", + "--audit-dir", + "/var/lib/d2b/guest-audit", + ]); + + let (host, guest) = match (host, guest) { + (BrokerMode::Host(host), BrokerMode::Guest(guest)) => (host, guest), + other => panic!("unexpected broker modes: {other:?}"), + }; + + assert_ne!(host.authority_id, guest.authority_id); + assert_ne!(host.socket_path, guest.socket_path); + assert_ne!(host.state_dir, guest.state_dir); + assert_ne!(host.audit_dir, guest.audit_dir); + assert_ne!(host.d2bd_uid, guest.d2bd_uid); +} + +#[test] +fn requests_cannot_select_a_profile() { + let error = parse_command([ + "host".to_owned(), + "guest".to_owned(), + "--test-mode".to_owned(), + ]) + .expect_err("profile must be a process-start argument, not a request-like trailing token"); + assert!(format!("{error:?}").contains("unknown host flag")); +} + +#[test] +#[cfg(not(feature = "layer1-bootstrap"))] +fn host_and_guest_instances_keep_separate_runtime_bindings() { + let host = TestBroker::spawn_profile("host-instance-", "host-instance", "host", D2BD_UID); + let guest = TestBroker::spawn_profile("guest-instance-", "guest-instance", "guest", D2BD_UID); + + assert_ne!(host.pid(), guest.pid()); + assert_ne!(host.socket_path(), guest.socket_path()); + + let host_client = connect_seqpacket(host.socket_path()).expect("connect host broker"); + let guest_client = connect_seqpacket(guest.socket_path()).expect("connect guest broker"); + let envelope = |request| BrokerRequestEnvelope { + request, + caller_role: BrokerCallerRole::AdminUid { uid: D2BD_UID }, + test_peer_uid: Some(D2BD_UID), + audit_join: None, + }; + send_json_frame( + host_client.as_raw_fd(), + &envelope(BrokerRequest::Hello(HelloRequest { + client_version: "test-0".to_owned(), + supported_features: vec![], + })), + ) + .expect("send host hello"); + send_json_frame( + guest_client.as_raw_fd(), + &envelope(BrokerRequest::Hello(HelloRequest { + client_version: "test-0".to_owned(), + supported_features: vec![], + })), + ) + .expect("send guest hello"); + + let host_response: BrokerResponse = recv_json_frame(host_client.as_raw_fd()) + .expect("receive host hello") + .expect("host hello response"); + let guest_response: BrokerResponse = recv_json_frame(guest_client.as_raw_fd()) + .expect("receive guest hello") + .expect("guest hello response"); + let BrokerResponse::Hello(host_hello) = host_response else { + panic!("host should return Hello"); + }; + let BrokerResponse::Hello(guest_hello) = guest_response else { + panic!("guest should return Hello"); + }; + assert!(host_hello.capabilities.contains(&"Hello".to_owned())); + assert!(guest_hello.capabilities.contains(&"SpawnRunner".to_owned())); + assert!( + !guest_hello + .capabilities + .contains(&"ApplyNftables".to_owned()) + ); + assert_ne!(host.audit_path(), guest.audit_path()); +} diff --git a/packages/d2b-priv-broker/tests/security_key_broker.rs b/packages/d2b-broker/tests/security_key_broker.rs similarity index 97% rename from packages/d2b-priv-broker/tests/security_key_broker.rs rename to packages/d2b-broker/tests/security_key_broker.rs index 824d62623d..1a70d82bda 100644 --- a/packages/d2b-priv-broker/tests/security_key_broker.rs +++ b/packages/d2b-broker/tests/security_key_broker.rs @@ -11,8 +11,8 @@ use d2b_contracts_broker::broker_wire::{ BrokerRequest, BrokerResponse, OpenHidrawSecurityKeyRequest, OpenHidrawSecurityKeyResponse, }; use d2b_contracts_resource::v3::ResourceRef; -use d2b_priv_broker::ops::audit_op::OperationFields; -use d2b_priv_broker::{fd_passing::recv_fds, protocol::send_json_frame_with_fds}; +use d2b_broker::ops::audit_op::OperationFields; +use d2b_broker::{fd_passing::recv_fds, protocol::send_json_frame_with_fds}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::unistd::close; use std::os::fd::{AsRawFd, OwnedFd}; diff --git a/packages/d2b-priv-broker/tests/socket_activation.rs b/packages/d2b-broker/tests/socket_activation.rs similarity index 94% rename from packages/d2b-priv-broker/tests/socket_activation.rs rename to packages/d2b-broker/tests/socket_activation.rs index 055f182272..d2a4072e4c 100644 --- a/packages/d2b-priv-broker/tests/socket_activation.rs +++ b/packages/d2b-broker/tests/socket_activation.rs @@ -30,10 +30,10 @@ use std::path::PathBuf; use std::process::Command; use std::time::Duration; +use d2b_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; use d2b_contracts_broker::broker_wire::{ BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, HelloRequest, }; -use d2b_priv_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; use nix::sys::socket::{ AddressFamily, Backlog, SockFlag, SockType, UnixAddr, bind, listen, socket, }; @@ -41,7 +41,7 @@ use tempfile::TempDir; /// Path to the broker binary under test; set by cargo when building /// integration tests for this package. -const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-priv-broker"); +const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-broker"); fn scratch_dir() -> TempDir { tempfile::tempdir().expect("tempdir") @@ -116,7 +116,7 @@ fn broker_adopts_socket_activated_fd_and_serves_hello() { let broker_bin = PathBuf::from(BROKER_BIN); assert!( broker_bin.exists(), - "broker binary not found at {}: set CARGO_BIN_EXE_d2b-priv-broker", + "broker binary not found at {}: set CARGO_BIN_EXE_d2b-broker", broker_bin.display() ); @@ -135,12 +135,14 @@ fn broker_adopts_socket_activated_fd_and_serves_hello() { let shell_cmd = format!( "LISTEN_PID=$$ LISTEN_FDS=1 LISTEN_FDNAMES=priv.sock \ exec 3>&{listen_fd_raw} \ - \"$BROKER\" serve \ + \"$BROKER\" host \ --test-mode \ --d2bd-uid \"$UID\" \ --d2bd-gid \"$GID\" \ --audit-dir \"$AUDIT_DIR\" \ - --bundle-path /nonexistent/bundle.json" + --bundle-path /nonexistent/bundle.json \ + --realm-controllers-path /nonexistent/realm-controllers.json \ + --realm-identity-path /nonexistent/realm-identity.json" ); let scrubber = scrub_shell_environment_path(); diff --git a/packages/d2b-priv-broker/tests/w12_fd_passing_response.rs b/packages/d2b-broker/tests/w12_fd_passing_response.rs similarity index 96% rename from packages/d2b-priv-broker/tests/w12_fd_passing_response.rs rename to packages/d2b-broker/tests/w12_fd_passing_response.rs index eb67d050a0..42c59459ce 100644 --- a/packages/d2b-priv-broker/tests/w12_fd_passing_response.rs +++ b/packages/d2b-broker/tests/w12_fd_passing_response.rs @@ -17,8 +17,8 @@ use std::os::fd::AsRawFd; -use d2b_priv_broker::fd_passing::recv_fds; -use d2b_priv_broker::protocol::{recv_json_frame, send_json_frame, send_json_frame_with_fds}; +use d2b_broker::fd_passing::recv_fds; +use d2b_broker::protocol::{recv_json_frame, send_json_frame, send_json_frame_with_fds}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::unistd::{close, pipe, read, write}; use serde::{Deserialize, Serialize}; diff --git a/packages/d2b-priv-broker/tests/w15_install_migrate.rs b/packages/d2b-broker/tests/w15_install_migrate.rs similarity index 98% rename from packages/d2b-priv-broker/tests/w15_install_migrate.rs rename to packages/d2b-broker/tests/w15_install_migrate.rs index 5e3d6a78ce..df9d608623 100644 --- a/packages/d2b-priv-broker/tests/w15_install_migrate.rs +++ b/packages/d2b-broker/tests/w15_install_migrate.rs @@ -2,6 +2,7 @@ use std::os::fd::AsRawFd; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_contracts::types::BundleOpId; +#[cfg(not(feature = "layer1-bootstrap"))] use d2b_contracts_broker::broker_wire::RunHostInstallRequest; use d2b_contracts_broker::broker_wire::{ BrokerErrorResponse, BrokerResponse, RunHostInstallResponse, RunMigrateResponse, @@ -17,9 +18,9 @@ use d2b_core::{ manifest_v04::ManifestV04, processes::ProcessesJson, }; -use d2b_priv_broker::protocol::{recv_json_frame, send_json_frame}; +use d2b_broker::protocol::{recv_json_frame, send_json_frame}; #[cfg(not(feature = "layer1-bootstrap"))] -use d2b_priv_broker::{ +use d2b_broker::{ ops::exec_reconcile::{IpRouteVerb, ReconcileExecError, ReconcileExecutor, UsbipSubcommand}, runtime::{dispatch_run_host_install_response, dispatch_run_host_install_response_for_intent}, }; @@ -59,7 +60,6 @@ fn installer_bundle_resolver(public_manifest_path: &str) -> BundleResolver { host_path: "/ignored/host.json".to_owned(), processes_path: "/ignored/processes.json".to_owned(), privileges_path: "/ignored/privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -80,7 +80,6 @@ fn installer_bundle_resolver(public_manifest_path: &str) -> BundleResolver { }; let processes = ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: Vec::new(), }; BundleResolver::from_artifacts(bundle, host, processes, manifest) @@ -227,7 +226,7 @@ impl ReconcileExecutor for FailingWriteExecutor { &self, _key_path: &Path, _comment: &str, - ) -> Result { + ) -> Result { unreachable!("RunHostInstall should not invoke ssh-keygen") } } diff --git a/packages/d2b-priv-broker/tests/zone_store.rs b/packages/d2b-broker/tests/zone_store.rs similarity index 95% rename from packages/d2b-priv-broker/tests/zone_store.rs rename to packages/d2b-broker/tests/zone_store.rs index 96530452b1..1ad6ec2963 100644 --- a/packages/d2b-priv-broker/tests/zone_store.rs +++ b/packages/d2b-broker/tests/zone_store.rs @@ -7,9 +7,9 @@ use d2b_contracts_broker::broker_wire::{ BrokerResponse, OpenZoneStoreResponse, ZoneStoreDisposition, }; use d2b_contracts_resource::v3::storage::ZoneStoreId; -use d2b_priv_broker::fd_passing::{FdPassingError, recv_one_fd, send_fds}; -use d2b_priv_broker::ops::audit_op::OperationFields; -use d2b_priv_broker::protocol::send_json_frame_with_fds; +use d2b_broker::fd_passing::{FdPassingError, recv_one_fd, send_fds}; +use d2b_broker::ops::audit_op::OperationFields; +use d2b_broker::protocol::send_json_frame_with_fds; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::unistd::{close, pipe}; diff --git a/packages/d2b-bus/BUILD.bazel b/packages/d2b-bus/BUILD.bazel index 0e054f617e..4d256d4a82 100644 --- a/packages/d2b-bus/BUILD.bazel +++ b/packages/d2b-bus/BUILD.bazel @@ -28,7 +28,9 @@ d2b_bus_srcs = glob(["src/**/*.rs"], allow_empty = True) d2b_bus_deps = [ "//packages/d2b-audit:d2b_audit", + "//packages/d2b-core-controller:d2b_core_controller", "//packages/d2b-contracts:d2b_contracts", + "//packages/d2b-contracts-provider:d2b_contracts_provider", "//packages/d2b-resource-api:d2b_resource_api", "//packages/d2b-resource-store:d2b_resource_store", "//packages/d2b-session:d2b_session", @@ -66,9 +68,11 @@ rust_library( ], visibility = ["//visibility:public"], deps = [ + "//packages/d2b-contracts-provider:d2b_contracts_provider", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-audit:d2b_audit_test_support", + "//packages/d2b-core-controller:d2b_core_controller_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-resource-api:d2b_resource_api_test_support", "//packages/d2b-resource-store:d2b_resource_store_test_support", @@ -110,7 +114,7 @@ rust_test( "//packages/d2b-resource-store:d2b_resource_store", "//packages/d2b-resource-store-redb:d2b_resource_store_redb", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], ) rust_test( diff --git a/packages/d2b-bus/Cargo.toml b/packages/d2b-bus/Cargo.toml index 3544e15461..8ba3cd7e17 100644 --- a/packages/d2b-bus/Cargo.toml +++ b/packages/d2b-bus/Cargo.toml @@ -17,6 +17,7 @@ d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0- async-trait = "0.1" d2b-audit = { path = "../d2b-audit", version = "0.0.0-bootstrap" } d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } +d2b-core-controller = { path = "../d2b-core-controller", version = "0.0.0-bootstrap" } d2b-resource-api = { path = "../d2b-resource-api", version = "0.0.0-bootstrap" } d2b-resource-store = { path = "../d2b-resource-store", version = "0.0.0-bootstrap" } d2b-telemetry = { path = "../d2b-telemetry", version = "0.0.0-bootstrap" } @@ -30,11 +31,14 @@ tokio = { workspace = true, features = ["io-util", "rt", "sync", "time"] } [dev-dependencies] d2b-controller-toolkit = { path = "../d2b-controller-toolkit", version = "0.0.0-bootstrap" } +d2b-contracts-provider = { path = "../d2b-contracts-provider", version = "0.0.0-bootstrap" } d2b-resource-store-redb = { path = "../d2b-resource-store-redb", version = "0.0.0-bootstrap" } quote = "1" syn = { version = "2", features = ["full", "visit"] } tempfile = "3" tokio = { workspace = true, features = ["macros", "rt", "sync", "test-util"] } +protobuf = "3.7.2" +ttrpc = { workspace = true, features = ["async"] } [[test]] name = "production_watch_rss" diff --git a/packages/d2b-bus/src/authorization.rs b/packages/d2b-bus/src/authorization.rs index 8c1560aa3e..505707d20b 100644 --- a/packages/d2b-bus/src/authorization.rs +++ b/packages/d2b-bus/src/authorization.rs @@ -12,6 +12,9 @@ use d2b_resource_api::authz::{ AuthorizationDenial, AuthorizationPolicyError, AuthorizationState, NativeAuthorizer, PolicySet, SessionVerb, }; +use d2b_core_controller::controller_assignment::{ + AssignmentError, AssignmentVerb, ControllerAssignmentRegistry, +}; use d2b_session::{ OperationMember, SessionAuthorizationRequest, SessionError, SessionOperation, contract::{AuthorizationLease, SessionErrorCode}, @@ -30,6 +33,7 @@ struct AuthorizationRuntime { /// Single-owner native authorizer and trusted policy state for one bus. pub struct BusAuthorizer { runtime: Mutex, + assignments: Option>>, } impl BusAuthorizer { @@ -52,9 +56,20 @@ impl BusAuthorizer { } Ok(Self { runtime: Mutex::new(AuthorizationRuntime { native, state }), + assignments: None, }) } + /// Bind the Core-owned assignment registry used to fence controller + /// watches and mutations at the existing bus admission seam. + pub fn with_assignment_registry( + mut self, + assignments: std::sync::Arc>, + ) -> Self { + self.assignments = Some(assignments); + self + } + /// Borrow the single native authorizer shared with the Resource API. /// /// The bus and generated resource handlers must evaluate the same policy @@ -130,6 +145,7 @@ impl BusAuthorizer { } if let Some(call) = resource_call { + self.authorize_assignment(context, call)?; let request = call.authorization_request(route.zone().clone())?; runtime .native @@ -138,6 +154,46 @@ impl BusAuthorizer { Ok(()) } + fn authorize_assignment( + &self, + context: &AuthenticatedSubjectContext, + call: &ResourceCall, + ) -> Result<(), AuthorizationError> { + let Some(assignment) = call.assignment() else { + return Ok(()); + }; + if context.reconnect_generation() != assignment.session_generation() + || context.provider_generation() != Some(assignment.provider_generation()) + || context.controller_generation() != Some(assignment.controller_generation()) + { + return Err(AuthorizationError::Assignment( + AssignmentError::SessionBindingMismatch, + )); + } + if let Some(target) = assignment.target().execution_ref() + && context.execution_ref() != Some(target) + && context.subject_ref() != target + { + return Err(AuthorizationError::Assignment( + AssignmentError::TargetMismatch, + )); + } + let verb = match call { + ResourceCall::List(_) => AssignmentVerb::List, + ResourceCall::Watch(_) => AssignmentVerb::Watch, + ResourceCall::ScopedCommitBatch { .. } => AssignmentVerb::CommitBatch, + _ => return Err(AuthorizationError::Assignment(AssignmentError::VerbNotAllowed)), + }; + if let Some(registry) = &self.assignments { + registry + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .validate_scope(assignment, verb) + .map_err(AuthorizationError::Assignment)?; + } + Ok(()) + } + pub(crate) fn authorize_cancel( &self, context: &AuthenticatedSubjectContext, @@ -313,6 +369,7 @@ pub enum AuthorizationError { Native(AuthorizationDenial), Policy(AuthorizationPolicyError), InvalidResourceCall, + Assignment(AssignmentError), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -371,6 +428,7 @@ impl AuthorizationError { Self::ZoneMismatch | Self::SessionBindingMismatch | Self::DiagnosticBinding + | Self::Assignment(_) | Self::Native(AuthorizationDenial::ZoneMismatch) => { AuthorizationErrorClass::SessionBinding } @@ -466,16 +524,26 @@ impl From for AuthorizationError { #[cfg(test)] mod tests { + use d2b_contracts_provider::v3::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ProviderManifest, + ResourceApiBinding, RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, + UpgradeDisposition, UpgradePolicy, + }; use d2b_contracts_resource::v3::{ ConfigurationGeneration, ControllerGeneration, + ResourceEnvelope, ResourceGeneration, ResourceRef, ResourceTypeName, ResourceUid, SchemaFingerprint, + SchemaVersion, ZoneRevision, }; +use d2b_contracts_resource::v3::execution_policy::BoundedToken; use d2b_contracts_resource::v3::identity::{ BindingDigest, ReconnectGeneration, @@ -490,6 +558,10 @@ use d2b_contracts_resource::v3::identity::{ PolicyRule, RelayGrantAuthority, ResourceVerb, }; use d2b_resource_store::PolicySnapshot; + use d2b_core_controller::controller_assignment::{ + AssignmentIdentity, AssignmentRequest, ControllerAssignmentRegistry, ControllerRoleContract, + ScopedResourceMutation, ScopedResourceQuery, + }; use super::*; use crate::{ @@ -531,6 +603,16 @@ use d2b_contracts_resource::v3::identity::{ service: &str, locality: Locality, evidence: EvidenceClass, + ) -> AuthenticatedSubjectContext { + context_with_session(zone, service, locality, evidence, 1) + } + + fn context_with_session( + zone: &str, + service: &str, + locality: Locality, + evidence: EvidenceClass, + session_generation: u64, ) -> AuthenticatedSubjectContext { AuthenticatedSubjectContext::new( ResourceRef::parse(if locality == Locality::Local { @@ -550,7 +632,7 @@ use d2b_contracts_resource::v3::identity::{ locality, BindingDigest::parse(format!("sha256:{}", "2".repeat(64))).unwrap(), ), - ReconnectGeneration::new(1).unwrap(), + ReconnectGeneration::new(session_generation).unwrap(), TranscriptHash::from_bytes([3; 32]), ), ) @@ -560,6 +642,15 @@ use d2b_contracts_resource::v3::identity::{ } fn route(zone: &str, service: &str, member: RouteMember) -> RouteKey { + route_with_session(zone, service, member, 1) + } + + fn route_with_session( + zone: &str, + service: &str, + member: RouteMember, + session_generation: u64, + ) -> RouteKey { RouteKey::new( ZoneId::parse(zone).unwrap(), ServiceName::parse(service).unwrap(), @@ -569,7 +660,7 @@ use d2b_contracts_resource::v3::identity::{ RouteGenerations::new( Some(ResourceGeneration::new(2).unwrap()), Some(ControllerGeneration::new(3).unwrap()), - ReconnectGeneration::new(1).unwrap(), + ReconnectGeneration::new(session_generation).unwrap(), ), ) } @@ -654,6 +745,430 @@ use d2b_contracts_resource::v3::identity::{ BusAuthorizer::new(native, state(1)).unwrap() } + const ASSIGNMENT_DIGEST: &str = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + + fn assignment_digest() -> ArtifactDigest { + ArtifactDigest::parse(ASSIGNMENT_DIGEST).unwrap() + } + + fn assignment_fingerprint() -> SchemaFingerprint { + SchemaFingerprint::parse(ASSIGNMENT_DIGEST).unwrap() + } + + fn assignment_manifest() -> ProviderManifest { + let process = ResourceTypeName::parse("Process").unwrap(); + let component = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [process.clone()], + [], + [d2b_contracts_resource::v3::ExecutionDomain::System], + 8, + assignment_digest(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + Default::default(), + None, + None, + d2b_contracts_resource::v3::PlacementAnchor::ExecutionRef, + ) + .unwrap(); + let trust = TrustEvidence { + publisher: BoundedToken::parse("trusted").unwrap(), + root_epoch: 1, + publisher_trusted: true, + signature: SignatureState::Valid, + revocation: RevocationState::Clear, + emergency_deny: false, + provenance: PolicyEvaluation::Accepted, + sbom: PolicyEvaluation::Accepted, + license: PolicyEvaluation::Accepted, + vulnerability: PolicyEvaluation::Accepted, + conformance: PolicyEvaluation::Accepted, + support_channel: BoundedToken::parse("stable").unwrap(), + }; + ProviderManifest::new( + d2b_contracts_resource::v3::ArtifactId::parse("provider-runtime").unwrap(), + ArtifactDigestSet { + package: assignment_digest(), + executable: assignment_digest(), + manifest: assignment_digest(), + config: assignment_digest(), + schema: assignment_digest(), + service: assignment_digest(), + }, + trust, + CompatibilityRange { + api_major: 3, + api_minor: 0, + descriptor_fingerprint: assignment_fingerprint(), + state_schema_version: SchemaVersion::new(1, 0).unwrap(), + }, + [component], + [binding], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + ]) + .unwrap() + } + + fn assignment_role() -> ControllerRoleContract { + ControllerRoleContract::from_signed_manifest( + ResourceRef::parse("Provider/system-core").unwrap(), + ResourceRef::parse("Process/process-controller").unwrap(), + &assignment_manifest(), + ) + .unwrap() + } + + fn assignment_resource() -> ResourceEnvelope { + let value = serde_json::json!({ + "apiVersion": "resources.d2bus.org/v3", + "type": "Process", + "metadata": { + "name": "process-one", + "zone": "dev", + "uid": "123e4567-e89b-42d3-a456-426614174000", + "generation": 1, + "revision": 1, + "ownerRef": null, + "finalizers": [], + "deletionRequestedAt": null, + "createdAt": "2026-07-22T00:00:00.000Z", + "updatedAt": "2026-07-22T00:00:00.000Z", + "managedBy": "api", + "configurationGeneration": null, + "controllerGeneration": null, + "providerGeneration": null + }, + "spec": { + "providerRef": "Provider/system-core", + "executionRef": "Host/host-system", + "argv": ["true"] + }, + "status": { + "completedAt": null, + "conditions": [], + "lastReconciledAt": null, + "observedGeneration": 0, + "outcome": null, + "phase": "Pending", + "resource": {}, + "startedAt": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": null, + "observedGeneration": 0, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + } + } + }); + ResourceEnvelope::from_json(&serde_json::to_vec(&value).unwrap()).unwrap() + } + + fn assignment_fixture() -> ( + AuthenticatedSubjectContext, + std::sync::Arc>, + AssignmentIdentity, + ScopedResourceQuery, + ScopedResourceMutation, + ScopedResourceMutation, + ) { + let resource = assignment_resource(); + let role = assignment_role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + true, + )) + .unwrap(); + let query = lease + .query( + vec![ResourceTypeName::parse("Process").unwrap()], + vec![resource.metadata().name().clone()], + Vec::new(), + ) + .unwrap(); + let mutation = lease + .mutation( + ResourceRef::parse("Process/process-one").unwrap(), + AssignmentVerb::UpdateStatus, + ) + .unwrap(); + let unsupported = lease + .mutation( + ResourceRef::parse("Process/process-one").unwrap(), + AssignmentVerb::Get, + ) + .unwrap(); + let identity = lease.identity().clone(); + let context = context_with_session( + "dev", + "d2b.resource.v3", + Locality::Local, + EvidenceClass::UnixPeer, + 4, + ) + .with_execution_ref(ResourceRef::parse("Host/host-system").unwrap()); + ( + context, + std::sync::Arc::new(Mutex::new(registry)), + identity, + query, + mutation, + unsupported, + ) + } + + fn assignment_authorizer( + context: &AuthenticatedSubjectContext, + registry: std::sync::Arc>, + resource_verbs: &[ResourceVerb], + ) -> BusAuthorizer { + let native = NativeAuthorizer::new( + ApiCatalog::standard(), + Some(policy( + 1, + context, + &[ + SessionVerb::Connect, + SessionVerb::Invoke, + SessionVerb::OpenStream, + ], + &[ResourceTypeName::parse("Process").unwrap()], + resource_verbs, + &[], + )), + ) + .unwrap(); + BusAuthorizer::new(native, state(1)) + .unwrap() + .with_assignment_registry(registry) + } + + #[test] + fn assignment_authorization_uses_registry_for_scoped_commit_and_watch() { + let (context, registry, identity, query, mutation, unsupported) = assignment_fixture(); + let watch_call = ResourceCall::Watch(ResourceQuery::from_scoped(query).unwrap()); + let commit_call = ResourceCall::ScopedCommitBatch { + assignment: identity.clone(), + mutations: vec![mutation], + }; + let authorizer = assignment_authorizer( + &context, + std::sync::Arc::clone(®istry), + &[ + ResourceVerb::Watch, + ResourceVerb::UpdateStatus, + ResourceVerb::Delete, + ], + ); + let watch_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 4, + ); + let commit_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::method("ResourceService/CommitBatch").unwrap(), + 4, + ); + assert_eq!( + authorizer.authorize_dispatch(&context, &watch_route, Some(&watch_call), true), + Ok(()) + ); + assert_eq!( + authorizer.authorize_dispatch(&context, &commit_route, Some(&commit_call), false), + Ok(()) + ); + + let unsupported_call = ResourceCall::ScopedCommitBatch { + assignment: identity, + mutations: vec![unsupported], + }; + assert_eq!( + authorizer.authorize_dispatch( + &context, + &commit_route, + Some(&unsupported_call), + false, + ), + Err(AuthorizationError::InvalidResourceCall) + ); + } + + #[test] + fn assignment_authorization_rejects_session_and_target_mismatch() { + let (valid_context, registry, _identity, query, _, _) = assignment_fixture(); + let call = ResourceCall::Watch(ResourceQuery::from_scoped(query).unwrap()); + let route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 1, + ); + let session_mismatch = context( + "dev", + "d2b.resource.v3", + Locality::Local, + EvidenceClass::UnixPeer, + ) + .with_execution_ref(ResourceRef::parse("Host/host-system").unwrap()); + let authorizer = assignment_authorizer( + &session_mismatch, + std::sync::Arc::clone(®istry), + &[ResourceVerb::Watch], + ); + assert_eq!( + authorizer.authorize_dispatch(&session_mismatch, &route, Some(&call), true), + Err(AuthorizationError::Assignment( + AssignmentError::SessionBindingMismatch + )) + ); + + let target_mismatch = valid_context + .clone() + .with_execution_ref(ResourceRef::parse("Host/other").unwrap()); + let target_authorizer = assignment_authorizer( + &target_mismatch, + registry, + &[ResourceVerb::Watch], + ); + let route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 4, + ); + assert_eq!( + target_authorizer.authorize_dispatch(&target_mismatch, &route, Some(&call), true), + Err(AuthorizationError::Assignment( + AssignmentError::TargetMismatch + )) + ); + } + + #[test] + fn assignment_authorization_rejects_revoked_and_stale_registry_scope() { + let (context, registry, identity, _query, mutation, _) = assignment_fixture(); + let commit_call = ResourceCall::ScopedCommitBatch { + assignment: identity.clone(), + mutations: vec![mutation], + }; + let commit_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::method("ResourceService/CommitBatch").unwrap(), + 4, + ); + registry + .lock() + .unwrap() + .revoke_session(ReconnectGeneration::new(4).unwrap()); + let revoked_authorizer = assignment_authorizer( + &context, + std::sync::Arc::clone(®istry), + &[ResourceVerb::UpdateStatus], + ); + assert_eq!( + revoked_authorizer.authorize_dispatch( + &context, + &commit_route, + Some(&commit_call), + false, + ), + Err(AuthorizationError::Assignment( + AssignmentError::SessionRevoked + )) + ); + + let (context, registry, identity, query, _, _) = assignment_fixture(); + let watch_call = ResourceCall::Watch(ResourceQuery::from_scoped(query).unwrap()); + registry + .lock() + .unwrap() + .begin_drain(&identity) + .unwrap(); + let stale_authorizer = + assignment_authorizer(&context, registry, &[ResourceVerb::Watch]); + let watch_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 4, + ); + assert_eq!( + stale_authorizer.authorize_dispatch(&context, &watch_route, Some(&watch_call), true), + Err(AuthorizationError::Assignment( + AssignmentError::StaleAssignment + )) + ); + } + #[test] fn cross_zone_dispatch_fails_only_on_authenticated_zone_inequality() { let claims = context( diff --git a/packages/d2b-bus/src/production_rss.rs b/packages/d2b-bus/src/production_rss.rs index 2c337e12d6..2c1f06589b 100644 --- a/packages/d2b-bus/src/production_rss.rs +++ b/packages/d2b-bus/src/production_rss.rs @@ -8,28 +8,13 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; -use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, - ResourceGeneration, - ResourceRef, - ResourceTypeName, - ResourceUid, - SchemaFingerprint, - ZoneId, - ZoneRevision, -}; use d2b_contracts_resource::v3::identity::{ - AuthenticatedSubjectContext, - BindingDigest, - EvidenceClass, - Locality, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, + AuthenticatedSubjectContext, BindingDigest, EvidenceClass, Locality, ReconnectGeneration, + ServiceName, SessionBinding, SessionPurpose, TranscriptHash, TransportBinding, +}; +use d2b_contracts_resource::v3::{ + ConfigurationGeneration, ControllerGeneration, ResourceGeneration, ResourceRef, + ResourceTypeName, ResourceUid, SchemaFingerprint, ZoneId, ZoneRevision, }; use d2b_resource_api::authz::{ ApiCatalog, BindingScope, BootstrapPhase, BoundSubject, CompiledRole, CompiledRoleBinding, diff --git a/packages/d2b-bus/src/router.rs b/packages/d2b-bus/src/router.rs index 6238a47a3d..1da6898cc5 100644 --- a/packages/d2b-bus/src/router.rs +++ b/packages/d2b-bus/src/router.rs @@ -25,6 +25,10 @@ use d2b_contracts_resource::v3::identity::{ ServiceName, SessionBinding, }; +use d2b_core_controller::controller_assignment::{ + AssignmentIdentity, AssignmentVerb, ScopedCommitTransport, ScopedResourceFilter, + ScopedResourceMutation, ScopedResourceQuery, +}; use d2b_resource_api::authz::{ ApiMethod, AuthorizationRequest, AuthorizationState, AuthorizationTarget, PolicySet, ResourceVerb, SessionVerb, @@ -33,7 +37,8 @@ use d2b_resource_api::watch::{WatchFrame, WatchSink, WatchSinkError}; use d2b_session::{ AuthenticatedComponentSession, AuthenticatedSessionRouteBinding, AuthenticatedTtrpcHandle, GENERATED_OPERATION_CATALOG, OperationKind, SessionAcceptor, SessionAuthorizationRequest, - SessionCancellationHandle, SessionOperation, SessionRegistrationCapability, + SessionCancellationHandle, SessionDriverHandle, SessionOperation, + SessionRegistrationCapability, contract::{EndpointPolicy, ServicePackage}, resource_operation, rewrite_ttrpc_stream_id, ttrpc_request_id, ttrpc_stream_id, }; @@ -217,6 +222,7 @@ pub struct ResourceQuery { resource_types: Vec, resource_names: Vec, filters: Vec, + assignment: Option, } impl ResourceQuery { @@ -237,6 +243,36 @@ impl ResourceQuery { resource_types, resource_names, filters, + assignment: None, + }) + } + + /// Consume a controller-minted query without allowing its assignment + /// filter to be dropped or widened. + pub fn from_scoped(query: ScopedResourceQuery) -> Result { + let (assignment, resource_types, resource_names, scoped_filters) = query.into_parts(); + if resource_types.is_empty() + || resource_types.len() > 64 + || resource_names.len() > 64 + || scoped_filters.len() > 64 + { + return Err(BusError::InvalidResourceCall); + } + let filters = scoped_filters + .into_iter() + .map(resource_filter_from_scoped) + .collect::, _>>()?; + if !filters + .iter() + .any(|filter| filter.field == "assignment.resourceUid") + { + return Err(BusError::InvalidResourceCall); + } + Ok(Self { + resource_types, + resource_names, + filters, + assignment: Some(assignment), }) } @@ -254,6 +290,11 @@ impl ResourceQuery { pub fn filters(&self) -> &[ResourceFilter] { &self.filters } + + /// Borrow the assignment evidence, when this query is controller-scoped. + pub const fn assignment(&self) -> Option<&AssignmentIdentity> { + self.assignment.as_ref() + } } impl core::fmt::Debug for ResourceQuery { @@ -279,6 +320,10 @@ pub enum ResourceCall { UpdateFinalizers(ResourceRef), Delete(ResourceRef), CommitBatch(Vec<(ResourceRef, ResourceVerb)>), + ScopedCommitBatch { + assignment: AssignmentIdentity, + mutations: Vec, + }, ResolveRef(ResourceRef), InspectSchema(ResourceTypeName), Upgrade(ResourceRef), @@ -360,6 +405,39 @@ impl ResourceCall { .collect(), ) } + Self::ScopedCommitBatch { + assignment, + mutations, + } => { + if mutations.is_empty() + || mutations.len() > 128 + || mutations.iter().any(|mutation| { + mutation.assignment() != assignment + || !mutation.verb().is_mutating() + || mutation.verb() == AssignmentVerb::CommitBatch + }) + { + return Err(BusError::InvalidResourceCall); + } + ( + ApiMethod::CommitBatch, + mutations + .iter() + .map(|mutation| { + let subresource = match mutation.verb() { + AssignmentVerb::UpdateStatus => Some("status"), + AssignmentVerb::UpdateFinalizers => Some("finalizers"), + _ => None, + }; + exact_target( + mutation.target(), + resource_verb_for_assignment(mutation.verb()), + subresource, + ) + }) + .collect(), + ) + } Self::ResolveRef(target) => ( ApiMethod::ResolveRef, vec![exact_target(target, ResourceVerb::Get, None)], @@ -402,12 +480,30 @@ impl ResourceCall { Self::UpdateFinalizers(_) => ApiMethod::UpdateFinalizers, Self::Delete(_) => ApiMethod::Delete, Self::CommitBatch(_) => ApiMethod::CommitBatch, + Self::ScopedCommitBatch { .. } => ApiMethod::CommitBatch, Self::ResolveRef(_) => ApiMethod::ResolveRef, Self::InspectSchema(_) => ApiMethod::InspectSchema, Self::Upgrade(_) => ApiMethod::Upgrade, } } + /// Borrow the assignment evidence attached to this resource call. + pub fn assignment(&self) -> Option<&AssignmentIdentity> { + match self { + Self::List(query) | Self::Watch(query) => query.assignment(), + Self::ScopedCommitBatch { assignment, .. } => Some(assignment), + _ => None, + } + } + + /// Borrow the controller mutations carried by a scoped commit call. + pub fn scoped_mutations(&self) -> Option<&[ScopedResourceMutation]> { + match self { + Self::ScopedCommitBatch { mutations, .. } => Some(mutations), + _ => None, + } + } + fn session_target(&self) -> Option<&ResourceRef> { match self { Self::Get(target) @@ -419,7 +515,11 @@ impl ResourceCall { | Self::Delete(target) | Self::ResolveRef(target) | Self::Upgrade(target) => Some(target), - Self::List(_) | Self::Watch(_) | Self::CommitBatch(_) | Self::InspectSchema(_) => None, + Self::List(_) + | Self::Watch(_) + | Self::CommitBatch(_) + | Self::ScopedCommitBatch { .. } + | Self::InspectSchema(_) => None, } } @@ -437,7 +537,11 @@ impl ResourceCall { | Self::Delete(target) | Self::ResolveRef(target) | Self::Upgrade(target) => target == route_target, - Self::List(_) | Self::Watch(_) | Self::CommitBatch(_) | Self::InspectSchema(_) => false, + Self::List(_) + | Self::Watch(_) + | Self::CommitBatch(_) + | Self::ScopedCommitBatch { .. } + | Self::InspectSchema(_) => false, } } } @@ -454,7 +558,7 @@ impl core::fmt::Debug for ResourceCall { Self::UpdateMetadata(_) => "UpdateMetadata", Self::UpdateFinalizers(_) => "UpdateFinalizers", Self::Delete(_) => "Delete", - Self::CommitBatch(_) => "CommitBatch", + Self::CommitBatch(_) | Self::ScopedCommitBatch { .. } => "CommitBatch", Self::ResolveRef(_) => "ResolveRef", Self::InspectSchema(_) => "InspectSchema", Self::Upgrade(_) => "Upgrade", @@ -488,7 +592,10 @@ fn query_targets(query: &ResourceQuery, verb: ResourceVerb) -> Vec Vec Vec Result { + if filter.field().is_empty() + || filter.field().len() > 64 + || !filter + .field() + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')) + || filter.values().is_empty() + || filter.values().len() > 64 + || (filter.field() == "assignment.resourceUid" && !filter.assignment_bound()) + { + return Err(BusError::InvalidResourceCall); + } + Ok(ResourceFilter { + field: filter.field().to_owned(), + values: filter.values().to_vec(), + }) +} + +fn resource_verb_for_assignment(verb: AssignmentVerb) -> ResourceVerb { + match verb { + AssignmentVerb::Get => ResourceVerb::Get, + AssignmentVerb::List => ResourceVerb::List, + AssignmentVerb::Watch => ResourceVerb::Watch, + AssignmentVerb::Create => ResourceVerb::Create, + AssignmentVerb::UpdateSpec => ResourceVerb::UpdateSpec, + AssignmentVerb::UpdateStatus => ResourceVerb::UpdateStatus, + AssignmentVerb::UpdateMetadata => ResourceVerb::UpdateMetadata, + AssignmentVerb::UpdateFinalizers => ResourceVerb::UpdateFinalizers, + AssignmentVerb::Delete => ResourceVerb::Delete, + AssignmentVerb::CommitBatch => unreachable!("batch wrapper is not a mutation target"), + } +} + /// Method invocation delivered only after exact route and authorization checks. pub struct DeliveredInvocation { route: RouteKey, @@ -1328,7 +1472,7 @@ impl UnixSubjectRecord { mut self, execution_ref: ResourceRef, ) -> d2b_session::Result { - if execution_ref.resource_type().as_str() != "Host" { + if !matches!(execution_ref.resource_type().as_str(), "Host" | "Guest") { return Err(d2b_session::SessionError::new( d2b_session::contract::SessionErrorCode::SubjectMismatch, )); @@ -1378,6 +1522,7 @@ impl UnixSubjectRecord { ResourceRef::parse("Provider/clipboard-wayland").ok() } "d2b.notification.v3" => ResourceRef::parse("Provider/notification-desktop").ok(), + "d2b.config-nixos.v3" => ResourceRef::parse("Provider/config-nixos").ok(), _ => self.provider_ref, } } else { @@ -1522,7 +1667,8 @@ impl core::fmt::Debug for AuthoritativeUnixSubjectResolver { } } -/// Trusted committed state used to install daemon-owned interaction subjects. +/// Trusted committed state used to install daemon-owned interaction and +/// Process/Shell attach subjects. pub struct CommittedInteractionSubjectInput { /// The Guest identity committed by the display WaylandSession. pub display_subject_ref: ResourceRef, @@ -2279,6 +2425,23 @@ impl crate::registry::BusEndpoint for ComponentEndpoint { let mut outbound_frame = request.payload().to_vec(); rewrite_ttrpc_stream_id(&mut outbound_frame, internal_stream_id) .map_err(|_| EndpointError::Rejected)?; + if let Some(ResourceCall::ScopedCommitBatch { + assignment, + mutations, + }) = request.resource_call() + { + let transport = ScopedCommitTransport::new(assignment.clone(), mutations.clone()) + .map_err(|_| EndpointError::Rejected)?; + outbound_frame = + d2b_resource_api::attach_scoped_commit_frame(&outbound_frame, &transport) + .map_err(|_| EndpointError::Rejected)?; + } else if matches!( + request.resource_call(), + Some(ResourceCall::CommitBatch(_)) + ) { + d2b_resource_api::reject_scoped_commit_frame(&outbound_frame) + .map_err(|_| EndpointError::Rejected)?; + } let request_id = ttrpc_request_id(self.generation, &outbound_frame) .map_err(|_| EndpointError::Rejected)?; let target = request @@ -2433,10 +2596,11 @@ impl ZoneRegistrar { ) } - /// Install the daemon-owned interaction subject projection from the - /// committed resource snapshot. The Unix peer UID authenticates the - /// transport only; the Guest/Host refs, resource generations, and - /// controller generation are all supplied by trusted committed state. + /// Install the daemon-owned interaction and Process/Shell subject + /// projection from the committed resource snapshot. The Unix peer UID + /// authenticates the transport only; the Guest/Host refs, resource + /// generations, and controller generation are all supplied by trusted + /// committed state. pub fn install_committed_interaction_subject( &self, committed: CommittedInteractionSubjectInput, @@ -2459,7 +2623,7 @@ impl ZoneRegistrar { ResourceRef::parse("Provider/display-wayland").expect("fixed display Provider ref"), display_generation, )]; - let mut subjects = Vec::with_capacity(5); + let mut subjects = Vec::with_capacity(6); for (service, provider_ref, generation) in services { subjects.push( UnixSubjectRecord::guest_for_uid( @@ -2474,6 +2638,21 @@ impl ZoneRegistrar { .for_service(service), ); } + // Process and ShellSession named streams use the generic Provider + // package on the wire, but their listener identities are retained by + // d2bd. Bind that package to the enrolled Guest's own execution + // reference rather than borrowing the display Host route. + subjects.push( + UnixSubjectRecord::guest_for_uid( + display_subject_ref.clone(), + display_subject_uid.clone(), + zone_ref.clone(), + expected_peer_uid, + )? + .with_controller_generation(controller_generation) + .with_execution_ref(display_subject_ref.clone())? + .for_service(ServicePackage::ProviderV3), + ); if let Some(generation) = clipboard_generation { let (subject_ref, subject_uid, provider_ref) = if let Some(provider_uid) = clipboard_provider_uid { @@ -2717,6 +2896,7 @@ fn routes_for_admitted_session( | "d2b.clipboard.bridge.v3" | "d2b.clipboard.picker-coord.v3" | "d2b.notification.v3" + | "d2b.config-nixos.v3" ); if binding.provider_ref().is_none() || (binding.subject_ref().resource_type().as_str() != "Provider" && !guest_provider_service) @@ -2888,6 +3068,14 @@ impl Drop for OperationLease { } impl BusIngress { + /// Clone the authenticated ComponentSession driver for a daemon-owned + /// target-local named-stream owner. + pub fn component_session_driver(&self) -> Option { + self.attachments + .as_ref() + .map(AuthenticatedTtrpcHandle::component_session_driver) + } + /// Clone the daemon-owned request receiver for one session loop. pub fn component_request_receiver(&self) -> ComponentRequestReceiver { ComponentRequestReceiver { @@ -3031,6 +3219,28 @@ impl BusIngress { result } + /// Invoke one assignment-scoped atomic commit over the existing Resource + /// bus route. + pub async fn invoke_scoped_commit_batch( + &self, + route: RouteKey, + operation: OperationSpec, + assignment: AssignmentIdentity, + mutations: Vec, + payload: Vec, + ) -> Result { + self.invoke_resource( + route, + operation, + ResourceCall::ScopedCommitBatch { + assignment, + mutations, + }, + payload, + ) + .await + } + async fn invoke_inner( &self, route: RouteKey, @@ -3651,7 +3861,7 @@ use d2b_contracts_resource::v3::identity::{ SessionPurpose, TranscriptHash, TransportBinding, -}; + }; use d2b_controller_toolkit::{ OperationContext, PendingQueue, PriorityLane, QueueHint, ResourceKey, TriggerReason, TriggerSet, @@ -4481,6 +4691,7 @@ use d2b_contracts_resource::v3::identity::{ remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(digest), @@ -6157,9 +6368,7 @@ use d2b_contracts_resource::v3::identity::{ #[test] fn endpoint_session_failures_preserve_actionable_details_and_closed_labels() { use crate::registry::EndpointFailureClass; - use d2b_contracts_zone_session::v3::{ - component_session::{Remediation, SessionErrorCode}, -}; + use d2b_contracts_zone_session::v3::component_session::{Remediation, SessionErrorCode}; let cases = [ ( diff --git a/packages/d2b-bus/src/session_seam_tests.rs b/packages/d2b-bus/src/session_seam_tests.rs index 02f2b4354c..1eb0cd1639 100644 --- a/packages/d2b-bus/src/session_seam_tests.rs +++ b/packages/d2b-bus/src/session_seam_tests.rs @@ -8,40 +8,57 @@ use std::{ use async_trait::async_trait; use d2b_bus::{ - BusAuthorizer, BusConfig, BusError, ComponentSessionAdmission, OperationId, OperationSpec, - ResourceCall, RouteGenerations, RouteKey, RouteMember, RouteTarget, ZoneBus, ZoneRegistrar, + AuthorizationError, BusAuthorizer, BusConfig, BusError, ComponentSessionAdmission, OperationId, + EndpointError, OperationSpec, ResourceCall, RouteGenerations, RouteKey, RouteMember, + RouteTarget, ZoneBus, ZoneRegistrar, }; -use d2b_contracts_zone_session::v3::{ - component_session::{AttachmentPolicy, CloseReason, EndpointPolicy, EndpointPurpose, EndpointRole, IdentityEvidenceRequirement, LimitProfile, Locality, NoiseProfile, PurposeClass, Remediation, ServicePackage, TransportBinding, TransportClass}, +use d2b_contracts_provider::v3::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ProviderManifest, ResourceApiBinding, + RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, UpgradeDisposition, + UpgradePolicy, +}; +use d2b_contracts_resource::v3::identity::{ + BindingDigest, EvidenceClass, ReconnectGeneration, ServiceName, }; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, - ResourceGeneration, - ResourceRef, - ResourceUid, - SchemaFingerprint, - ZoneId, + ConfigurationGeneration, ControllerGeneration, ExecutionDomain, PlacementAnchor, + ResourceGeneration, ResourceRef, ResourceUid, SchemaFingerprint, SchemaVersion, ZoneId, ZoneRevision, }; -use d2b_contracts_resource::v3::identity::{ - BindingDigest, - EvidenceClass, - ServiceName, +use d2b_contracts_zone_session::v3::component_session::{ + AttachmentPolicy, CloseReason, EndpointPolicy, EndpointPurpose, EndpointRole, + IdentityEvidenceRequirement, LimitProfile, Locality, NoiseProfile, PurposeClass, Remediation, + ServicePackage, TransportBinding, TransportClass, +}; +use d2b_core_controller::controller_assignment::{ + AssignmentError, AssignmentRequest, AssignmentVerb, ControllerAssignmentRegistry, + ControllerRoleContract, ScopedCommitTransport, }; use d2b_resource_api::authz::{ ApiCatalog, AuthorizationState, BindingScope, BootstrapPhase, BoundSubject, CompiledRole, CompiledRoleBinding, NativeAuthorizer, PolicyRule, PolicySet, RelayGrantAuthority, ResourceVerb, SessionVerb, }; +use d2b_resource_api::{ResourceBusAdapter, ResourceService, ResourceStoreBackend}; use d2b_resource_store::PolicySnapshot; +use d2b_resource_store::mutation_seal::{MutationSealAcceptor, MutationSealBody}; +use d2b_resource_store::{ + ResourceMutationKind, StoreCommitResult, StoreError, StoreGetRequest, + StoreInspectSchemaRequest, StoreListRequest, StoreListResult, StoreResolveRequest, + StoreResolvedIdentity, StoreSealIdentity, StoreSlot, StoreWatchReceipt, StoreWatchRequest, + StoredResource, StoredSchema, +}; use d2b_session::{ AuthenticatedComponentSession, ComponentSessionDriver, HandshakeCredentials, OwnedTransport, SessionDriverHandle, SessionEngine, TransportDescriptor, TransportError, TransportEvidence, - TransportPacket, ttrpc_request_id, ttrpc_stream_id, + TransportPacket, serve_ttrpc_services, ttrpc_request_id, ttrpc_stream_id, }; use d2b_session_unix::{SeqpacketSocket, VerifiedUnixPeer, prearmed_seqpacket_pair}; +use protobuf::Message; use tokio::sync::{Notify, mpsc}; +use ttrpc::proto::{MessageHeader, Request as TtrpcRequest}; use crate::router::UnixSubjectRecord; @@ -211,7 +228,28 @@ async fn admit( SessionDriverHandle, tokio::task::JoinHandle<()>, ) { - admit_with_writer_pause(registrar, policy, subject, uid, provider, _allowed, None).await + admit_inner( + registrar, policy, subject, uid, provider, _allowed, None, true, + ) + .await +} + +async fn admit_without_echo( + registrar: &ZoneRegistrar, + policy: EndpointPolicy, + subject: &str, + uid: &str, + provider: &str, + allowed: impl IntoIterator, +) -> ( + AuthenticatedComponentSession, + SessionDriverHandle, + tokio::task::JoinHandle<()>, +) { + admit_inner( + registrar, policy, subject, uid, provider, allowed, None, false, + ) + .await } async fn admit_with_writer_pause( @@ -226,6 +264,33 @@ async fn admit_with_writer_pause( AuthenticatedComponentSession, SessionDriverHandle, tokio::task::JoinHandle<()>, +) { + admit_inner( + registrar, + policy, + subject, + uid, + provider, + _allowed, + writer_pause, + true, + ) + .await +} + +async fn admit_inner( + registrar: &ZoneRegistrar, + policy: EndpointPolicy, + subject: &str, + uid: &str, + provider: &str, + _allowed: impl IntoIterator, + writer_pause: Option>, + start_echo: bool, +) -> ( + AuthenticatedComponentSession, + SessionDriverHandle, + tokio::task::JoinHandle<()>, ) { let descriptor = TransportDescriptor { class: policy.transport_binding.transport, @@ -262,14 +327,18 @@ async fn admit_with_writer_pause( ), ); let remote = responder.unwrap().into_driver(); - let echo_remote = remote.clone(); - let echo = tokio::spawn(async move { - while let Ok(frame) = echo_remote.receive_ttrpc().await { - if echo_remote.send_ttrpc(frame).await.is_err() { - break; + let echo = if start_echo { + let echo_remote = remote.clone(); + tokio::spawn(async move { + while let Ok(frame) = echo_remote.receive_ttrpc().await { + if echo_remote.send_ttrpc(frame).await.is_err() { + break; + } } - } - }); + }) + } else { + tokio::spawn(async {}) + }; let subject_ref = ResourceRef::parse(subject).unwrap(); let subject_uid = ResourceUid::parse(uid).unwrap(); let provider_ref = ResourceRef::parse(provider).unwrap(); @@ -303,6 +372,8 @@ async fn admit_with_writer_pause( ) .unwrap() .with_provider(provider_ref, provider_generation) + .unwrap() + .with_execution_ref(ResourceRef::parse("Host/host-system").unwrap()) .unwrap(), } .with_controller_generation(ControllerGeneration::new(CONTROLLER_GENERATION).unwrap()); @@ -455,6 +526,606 @@ fn route(service: &str, member: &str, generation: u64, provider: &str) -> RouteK ) } +const ASSIGNMENT_DIGEST: &str = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + +fn assignment_digest() -> ArtifactDigest { + ArtifactDigest::parse(ASSIGNMENT_DIGEST).unwrap() +} + +fn assignment_fingerprint() -> SchemaFingerprint { + SchemaFingerprint::parse(ASSIGNMENT_DIGEST).unwrap() +} + +fn assignment_manifest() -> ProviderManifest { + let process = d2b_contracts_resource::v3::ResourceTypeName::parse("Process").unwrap(); + let component = ComponentDescriptor::new( + d2b_contracts_resource::v3::execution_policy::BoundedToken::parse("process-controller") + .unwrap(), + ComponentType::Controller, + [process.clone()], + [], + [ExecutionDomain::System], + 1, + assignment_digest(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + Default::default(), + None, + None, + PlacementAnchor::ExecutionRef, + ) + .unwrap(); + let trust = TrustEvidence { + publisher: d2b_contracts_resource::v3::execution_policy::BoundedToken::parse("trusted") + .unwrap(), + root_epoch: 1, + publisher_trusted: true, + signature: SignatureState::Valid, + revocation: RevocationState::Clear, + emergency_deny: false, + provenance: PolicyEvaluation::Accepted, + sbom: PolicyEvaluation::Accepted, + license: PolicyEvaluation::Accepted, + vulnerability: PolicyEvaluation::Accepted, + conformance: PolicyEvaluation::Accepted, + support_channel: d2b_contracts_resource::v3::execution_policy::BoundedToken::parse( + "stable", + ) + .unwrap(), + }; + ProviderManifest::new( + d2b_contracts_resource::v3::ArtifactId::parse("provider-system-core").unwrap(), + ArtifactDigestSet { + package: assignment_digest(), + executable: assignment_digest(), + manifest: assignment_digest(), + config: assignment_digest(), + schema: assignment_digest(), + service: assignment_digest(), + }, + trust, + CompatibilityRange { + api_major: 3, + api_minor: 0, + descriptor_fingerprint: assignment_fingerprint(), + state_schema_version: SchemaVersion::new(1, 0).unwrap(), + }, + [component], + [binding], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + ]) + .unwrap() +} + +fn assignment_resource() -> d2b_contracts_resource::v3::ResourceEnvelope { + d2b_contracts_resource::v3::ResourceEnvelope::from_json( + br#"{"apiVersion":"resources.d2bus.org/v3","type":"Process","metadata":{"configurationGeneration":null,"createdAt":"2026-07-22T00:00:00.000Z","deletionRequestedAt":null,"finalizers":[],"generation":1,"managedBy":"api","name":"work","ownerRef":null,"revision":7,"uid":"123e4567-e89b-42d3-a456-426614174000","updatedAt":"2026-07-22T00:00:00.000Z","zone":"dev"},"spec":{"executionRef":"Host/host-system","providerRef":"Provider/system-core","argv":["true"]},"status":{"completedAt":null,"conditions":[],"lastReconciledAt":null,"observedGeneration":0,"outcome":null,"phase":"Pending","resource":{},"startedAt":null,"update":{"dependencies":{"count":0,"refs":[]},"disruption":"None","lastAssessedAt":null,"observedGeneration":0,"operationId":null,"owned":{"count":0,"refs":[]},"preserveState":true,"reasons":[],"state":"Unknown","targetGeneration":1}}}"#, + + ) + .unwrap() +} + +fn scoped_bus() -> ( + ZoneBus, + ZoneRegistrar, + std::sync::Arc>, + std::sync::Arc, + AuthorizationState, +) { + let catalog = ApiCatalog::standard(); + let zone = ZoneId::parse("dev").unwrap(); + let rule = PolicyRule::new( + &catalog, + [d2b_contracts_resource::v3::ResourceTypeName::parse("Process").unwrap()], + [ResourceVerb::UpdateStatus, ResourceVerb::UpdateFinalizers], + [ + SessionVerb::Connect, + SessionVerb::Invoke, + SessionVerb::Cancel, + ], + [], + [], + [zone.clone()], + [], + ) + .unwrap(); + let role = CompiledRole::new( + ResourceRef::parse("Role/scoped-commit").unwrap(), + vec![rule], + ) + .unwrap(); + let subjects = [ + ( + "Provider/system-core", + "11111111-1111-4111-8111-111111111111", + ), + ("Host/alice", "22222222-2222-4222-8222-222222222222"), + ] + .into_iter() + .map(|(subject, uid)| BoundSubject { + subject_ref: ResourceRef::parse(subject).unwrap(), + subject_uid: ResourceUid::parse(uid).unwrap(), + }); + let binding = CompiledRoleBinding::new( + role.role_ref.clone(), + subjects, + BindingScope::default(), + RelayGrantAuthority::None, + ) + .unwrap(); + let policy_set = PolicySet::new(&catalog, 1, vec![role], vec![binding]).unwrap(); + let native = std::sync::Arc::new(NativeAuthorizer::new(catalog, Some(policy_set)).unwrap()); + let state = AuthorizationState { + snapshot: PolicySnapshot { + policy_revision: 1, + api_catalog_revision: 1, + active_configuration_revision: ConfigurationGeneration::new(1).unwrap(), + controller_generation: Some(ControllerGeneration::new(CONTROLLER_GENERATION).unwrap()), + }, + zone_policy_revision: ZoneRevision::new(1), + bootstrap_phase: BootstrapPhase::Disabled, + now_tick: 1, + }; + let assignments = std::sync::Arc::new(std::sync::Mutex::new( + ControllerAssignmentRegistry::default(), + )); + let authorizer = BusAuthorizer::from_shared(std::sync::Arc::clone(&native), state.clone()) + .unwrap() + .with_assignment_registry(std::sync::Arc::clone(&assignments)); + let (bus, registrar) = ZoneBus::new(zone, authorizer, BusConfig::default()).unwrap(); + (bus, registrar, assignments, native, state) +} + +struct ScopedStore { + acceptor: MutationSealAcceptor, + commits: std::sync::Arc>>>, +} + +impl ResourceStoreBackend for ScopedStore { + async fn get(&self, _: StoreGetRequest) -> Result { + unreachable!("scoped commit proof does not read through the backend") + } + + async fn list(&self, _: StoreListRequest) -> Result { + unreachable!("scoped commit proof does not list through the backend") + } + + async fn watch(&self, _: StoreWatchRequest) -> Result { + unreachable!("scoped commit proof does not watch through the backend") + } + + async fn resolve_ref( + &self, + _: StoreResolveRequest, + ) -> Result { + unreachable!("scoped commit proof does not resolve refs through the backend") + } + + async fn inspect_schema( + &self, + _: StoreInspectSchemaRequest, + ) -> Result { + unreachable!("scoped commit proof does not inspect schemas through the backend") + } + + async fn commit_verified( + &self, + mutation: d2b_resource_store::SealedMutation, + ) -> Result { + let body: MutationSealBody = self.acceptor.open(mutation).unwrap().into_body(); + let observed = body + .mutations + .iter() + .map(|mutation| { + ( + mutation.mutation().kind, + mutation.mutation().assignment.is_some(), + ) + }) + .collect::>(); + self.commits.lock().unwrap().push(observed); + Ok(StoreCommitResult { + resources: Vec::new(), + revision: ZoneRevision::new(8), + }) + } +} + +fn commit_batch_frame(operation_id: &str) -> Vec { + let envelope = assignment_resource(); + let identity = d2b_contracts_resource::resource_proto::ResourceIdentity { + zone: "dev".to_owned(), + resource_type: "Process".to_owned(), + name: "work".to_owned(), + uid: Some(envelope.metadata().uid().as_str().to_owned()), + generation: Some(envelope.metadata().generation().get()), + revision: Some(envelope.metadata().revision().get()), + special_fields: protobuf::SpecialFields::new(), + }; + let mut precondition = d2b_contracts_resource::resource_proto::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + d2b_contracts_resource::resource_proto::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION, + ); + precondition.expected_revision = Some(envelope.metadata().revision().get()); + precondition.expected_uid = Some(envelope.metadata().uid().as_str().to_owned()); + let mut status = d2b_contracts_resource::resource_proto::Mutation::new(); + status.kind = protobuf::EnumOrUnknown::new( + d2b_contracts_resource::resource_proto::MutationKind::MUTATION_KIND_UPDATE_STATUS, + ); + status.target = protobuf::MessageField::some(identity.clone()); + status.precondition = protobuf::MessageField::some(precondition.clone()); + let mut status_body = d2b_contracts_resource::resource_proto::ResourceEnvelopeBytes::new(); + status_body.identity = protobuf::MessageField::some(identity.clone()); + status_body.canonical_json = envelope.canonical_bytes().unwrap(); + status_body.payload_digest = envelope.digest().unwrap(); + status.resource = protobuf::MessageField::some(status_body); + + let mut finalizers = d2b_contracts_resource::resource_proto::Mutation::new(); + finalizers.kind = protobuf::EnumOrUnknown::new( + d2b_contracts_resource::resource_proto::MutationKind::MUTATION_KIND_UPDATE_FINALIZERS, + ); + finalizers.target = protobuf::MessageField::some(identity); + finalizers.precondition = protobuf::MessageField::some(precondition); + finalizers + .add_finalizers + .push("process.d2bus.org/reconcile".to_owned()); + + let mut meta = d2b_contracts_resource::resource_proto::RequestMeta::new(); + meta.operation_id = operation_id.to_owned(); + meta.idempotency_key = format!("{operation_id}-key"); + meta.correlation_id = format!("{operation_id}-correlation"); + meta.deadline_ms = 10_000; + let request = d2b_contracts_resource::resource_proto::CommitBatchRequest { + meta: protobuf::MessageField::some(meta), + mutations: vec![status, finalizers], + scoped_admission: Vec::new(), + special_fields: protobuf::SpecialFields::new(), + }; + let rpc = TtrpcRequest { + service: "d2b.resource.v3.ResourceService".to_owned(), + method: "CommitBatch".to_owned(), + payload: request.write_to_bytes().unwrap(), + ..TtrpcRequest::default() + }; + let body = rpc.write_to_bytes().unwrap(); + let header = MessageHeader::new_request(1, body.len() as u32); + let mut frame = Vec::with_capacity(ttrpc::proto::MESSAGE_HEADER_LENGTH + body.len()); + frame.extend_from_slice(&Vec::from(header)); + frame.extend_from_slice(&body); + frame +} + +#[tokio::test] +async fn production_scoped_commit_chain_authorizes_and_fences_store_writes() { + let (_bus, mut registrar, assignments, native, state) = scoped_bus(); + let resource = assignment_resource(); + let role = ControllerRoleContract::from_signed_manifest( + ResourceRef::parse("Provider/system-core").unwrap(), + ResourceRef::parse("Process/process-controller").unwrap(), + &assignment_manifest(), + ) + .unwrap(); + let old_lease = assignments + .lock() + .unwrap() + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(PROVIDER_GENERATION).unwrap(), + ControllerGeneration::new(CONTROLLER_GENERATION).unwrap(), + ReconnectGeneration::new(1).unwrap(), + true, + )) + .unwrap(); + let old_identity = old_lease.identity().clone(); + let target = ResourceRef::parse("Process/work").unwrap(); + let old_mutations = vec![ + old_lease + .mutation(target.clone(), AssignmentVerb::UpdateStatus) + .unwrap(), + old_lease + .mutation(target.clone(), AssignmentVerb::UpdateFinalizers) + .unwrap(), + ]; + + let (resource_endpoint, resource_remote, resource_echo) = admit_without_echo( + ®istrar, + policy( + ServicePackage::ResourceV3, + EndpointPurpose::ResourceService, + 1, + ), + "Provider/system-core", + "11111111-1111-4111-8111-111111111111", + "Provider/system-core", + [SessionVerb::Invoke], + ) + .await; + let store_identity = StoreSealIdentity::new( + StoreSlot::new(0).unwrap(), + ZoneId::parse("dev").unwrap(), + ResourceUid::parse("99999999-9999-4999-8999-999999999999").unwrap(), + ); + let acceptor = native.take_store_seal(store_identity).unwrap(); + let commits = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let store = std::sync::Arc::new(ScopedStore { + acceptor, + commits: std::sync::Arc::clone(&commits), + }); + let service = std::sync::Arc::new( + ResourceService::new( + std::sync::Arc::clone(&store), + std::sync::Arc::clone(&native), + ) + .unwrap(), + ); + let endpoint_subject = native + .issue_authenticated_subject( + resource_endpoint.route_binding().context().clone(), + state.clone(), + ) + .unwrap(); + let adapter = std::sync::Arc::new( + ResourceBusAdapter::bind_component_session(service, endpoint_subject).unwrap(), + ); + let service_task = tokio::spawn(serve_ttrpc_services( + std::sync::Arc::new(resource_remote), + adapter.ttrpc_services(), + )); + for _ in 0..16 { + tokio::task::yield_now().await; + } + let _resource_ingress = registrar + .register_component_session(resource_endpoint) + .await + .unwrap(); + + let (caller, _caller_remote, caller_echo) = admit( + ®istrar, + policy( + ServicePackage::ResourceV3, + EndpointPurpose::ResourceService, + 1, + ), + "Host/alice", + "22222222-2222-4222-8222-222222222222", + "Provider/system-core", + [SessionVerb::Invoke], + ) + .await; + let caller = registrar.register_component_session(caller).await.unwrap(); + let route = route( + "d2b.resource.v3", + "ResourceService/CommitBatch", + 1, + "Provider/system-core", + ); + + assignments + .lock() + .unwrap() + .begin_drain(&old_identity) + .unwrap(); + let stale = caller + .invoke_scoped_commit_batch( + route.clone(), + OperationSpec::new(OperationId::parse("scoped-stale").unwrap(), 10_000).unwrap(), + old_identity.clone(), + old_mutations.clone(), + commit_batch_frame("scoped-stale"), + ) + .await + .unwrap_err(); + assert!(matches!( + stale, + BusError::Authorization(AuthorizationError::Assignment( + AssignmentError::StaleAssignment + )) + )); + + assignments.lock().unwrap().release(&old_identity).unwrap(); + let new_lease = assignments + .lock() + .unwrap() + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(PROVIDER_GENERATION).unwrap(), + ControllerGeneration::new(CONTROLLER_GENERATION).unwrap(), + ReconnectGeneration::new(1).unwrap(), + true, + )) + .unwrap(); + let new_identity = new_lease.identity().clone(); + let new_mutations = vec![ + new_lease + .mutation(target.clone(), AssignmentVerb::UpdateStatus) + .unwrap(), + new_lease + .mutation(target.clone(), AssignmentVerb::UpdateFinalizers) + .unwrap(), + ]; + let revoked_mutations = new_mutations.clone(); + let mut scoped_response = None; + for attempt in 0..32 { + let operation_id = format!("scoped-valid-{attempt}"); + let response = caller + .invoke_scoped_commit_batch( + route.clone(), + OperationSpec::new(OperationId::parse(&operation_id).unwrap(), 10_000).unwrap(), + new_identity.clone(), + new_mutations.clone(), + commit_batch_frame(&operation_id), + ) + .await + .unwrap(); + let response = ttrpc::proto::Response::parse_from_bytes( + &response.as_bytes()[ttrpc::proto::MESSAGE_HEADER_LENGTH..], + ) + .unwrap(); + let response = + d2b_contracts_resource::resource_proto::CommitBatchResponse::parse_from_bytes( + &response.payload, + ) + .unwrap(); + if response.error.is_some() { + scoped_response = Some(response); + break; + } + if response.revision == 8 { + scoped_response = Some(response); + break; + } + tokio::task::yield_now().await; + } + let response = scoped_response.expect("scoped commit response"); + assert!(response.error.is_none()); + assert_eq!(response.revision, 8); + + let forged_scope = + ScopedCommitTransport::new(new_identity.clone(), new_mutations.clone()).unwrap(); + let forged_frame = d2b_resource_api::attach_scoped_commit_frame( + &commit_batch_frame("plain-forged"), + &forged_scope, + ) + .unwrap(); + let forged = caller + .invoke_resource( + route.clone(), + OperationSpec::new(OperationId::parse("plain-forged").unwrap(), 10_000).unwrap(), + ResourceCall::CommitBatch(vec![ + (target.clone(), ResourceVerb::UpdateStatus), + (target.clone(), ResourceVerb::UpdateFinalizers), + ]), + forged_frame, + ) + .await + .unwrap_err(); + assert!(matches!(forged, BusError::Endpoint(EndpointError::Rejected))); + + for attempt in 0..8 { + if commits.lock().unwrap().len() >= 2 { + break; + } + let operation_id = format!("plain-commit-{attempt}"); + caller + .invoke_resource( + route.clone(), + OperationSpec::new(OperationId::parse(&operation_id).unwrap(), 10_000).unwrap(), + ResourceCall::CommitBatch(vec![ + (target.clone(), ResourceVerb::UpdateStatus), + (target.clone(), ResourceVerb::UpdateFinalizers), + ]), + commit_batch_frame(&operation_id), + ) + .await + .unwrap(); + for _ in 0..8 { + if commits.lock().unwrap().len() >= 2 { + break; + } + tokio::task::yield_now().await; + } + } + + assignments + .lock() + .unwrap() + .revoke_session(ReconnectGeneration::new(1).unwrap()); + let revoked = caller + .invoke_scoped_commit_batch( + route, + OperationSpec::new(OperationId::parse("scoped-revoked").unwrap(), 10_000).unwrap(), + new_identity, + revoked_mutations, + commit_batch_frame("scoped-revoked"), + ) + .await + .unwrap_err(); + assert!(matches!( + revoked, + BusError::Authorization(AuthorizationError::Assignment( + AssignmentError::SessionRevoked + )) + )); + + let commits = commits.lock().unwrap().clone(); + assert_eq!(commits.len(), 2); + assert_eq!( + commits[0], + vec![ + (ResourceMutationKind::UpdateStatus, true), + (ResourceMutationKind::UpdateFinalizers, true), + ] + ); + assert_eq!( + commits[1], + vec![ + (ResourceMutationKind::UpdateStatus, false), + (ResourceMutationKind::UpdateFinalizers, false), + ] + ); + assert!(commits[0].iter().all(|(_, fenced)| *fenced)); + assert!(commits[1].iter().all(|(_, fenced)| !*fenced)); + + service_task.abort(); + let _ = service_task.await; + resource_echo.abort(); + caller_echo.abort(); +} + fn ttrpc_frame(stream_id: u32, payload: &[u8]) -> Vec { let mut frame = Vec::with_capacity(10 + payload.len()); frame.extend_from_slice(&(payload.len() as u32).to_be_bytes()); diff --git a/packages/d2b-bus/tests/production_watch_rss.rs b/packages/d2b-bus/tests/production_watch_rss.rs index d7b897c79f..cefbb78972 100644 --- a/packages/d2b-bus/tests/production_watch_rss.rs +++ b/packages/d2b-bus/tests/production_watch_rss.rs @@ -218,6 +218,7 @@ fn mutation( remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(digest), diff --git a/packages/d2b-contracts-broker/BUILD.bazel b/packages/d2b-contracts-broker/BUILD.bazel index f09bc5cf8c..eff0613b29 100644 --- a/packages/d2b-contracts-broker/BUILD.bazel +++ b/packages/d2b-contracts-broker/BUILD.bazel @@ -31,19 +31,6 @@ rust_test( "//packages/d2b-contracts-resource:d2b_contracts_resource", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) -rust_test( - name = "cutover_wire", - srcs = ["tests/cutover_wire.rs"], - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - ":d2b_contracts_broker", - "//packages/d2b-contracts:d2b_contracts", - "//packages/d2b-contracts-control:d2b_contracts_control", - "//packages/d2b-contracts-resource:d2b_contracts_resource", - ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - rust_test( name = "d2b_contracts_broker_test", compile_data = ["Cargo.toml"], diff --git a/packages/d2b-contracts-broker/Cargo.toml b/packages/d2b-contracts-broker/Cargo.toml index f49e4511bd..6d95bba8b6 100644 --- a/packages/d2b-contracts-broker/Cargo.toml +++ b/packages/d2b-contracts-broker/Cargo.toml @@ -17,5 +17,4 @@ serde_json.workspace = true sha2.workspace = true [dev-dependencies] -d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } serde_json.workspace = true diff --git a/packages/d2b-contracts-broker/src/broker_wire.rs b/packages/d2b-contracts-broker/src/broker_wire.rs index 14580a49fe..23e8dcd58d 100644 --- a/packages/d2b-contracts-broker/src/broker_wire.rs +++ b/packages/d2b-contracts-broker/src/broker_wire.rs @@ -10,7 +10,6 @@ use d2b_contracts::audit_wire::validate_audit_page; pub use d2b_contracts::audit_wire::{AuditExportCursor, AuditExportEntry, AuditExportErrorCode}; -use d2b_contracts::auth_wire::AUTH_NONCE_LEN; pub use d2b_contracts::store_verify_wire::{ StoreVerifyRequest, StoreVerifyResponse, StoreVerifyStatus, StoreVerifyUnknownReason, }; @@ -23,7 +22,8 @@ use d2b_contracts_resource::v3::process::{ CapabilityClass, EnvironmentClass, NamespaceClass, UserNamespaceSpec, }; use d2b_contracts_resource::v3::{ - ArtifactId, IfName, ResourceBundleGenerationId, ResourceGeneration, ResourceRef, ResourceUid, + ActivationRunnerInput, ArtifactId, IfName, ResourceBundleGenerationId, ResourceGeneration, + ResourceRef, ResourceUid, execution_policy::ExecutionDomain, storage::ZoneStoreId, }; use schemars::JsonSchema; @@ -37,16 +37,6 @@ pub enum BrokerRequest { /// handoff. The broker resolves all host effects from its trusted /// installed-generation state; no path or command crosses the wire. ApplyHostGenerationHandoff(crate::host_generation::ApplyHostGenerationHandoff), - /// Launch one operation-scoped cutover runner before control-plane drain. - /// - /// The request must carry exactly one SCM_RIGHTS bootstrap descriptor. - /// The broker resolves the runner executable from its trusted server - /// configuration and never accepts a path or command over the wire. - LaunchCutoverRunner(LaunchCutoverRunnerRequest), - /// Append one durable, operation-scoped cutover audit record. - CutoverAudit(CutoverAuditRequest), - /// Dispatch one closed operation-scoped cutover or reset effect. - CutoverEffect(CutoverEffectRequest), ApplyNftables(ApplyNftablesRequest), /// Apply or remove one Provider-owned nftables projection. /// @@ -93,7 +83,6 @@ pub enum BrokerRequest { /// the bootstrap `Hello` shape so the connection layer doesn't need /// a side-channel. Hello(HelloRequest), - GuestControlSign(GuestControlSignRequest), InjectSecretById(SecretByIdRequest), LaunchMinijailChild(LaunchMinijailChildRequest), ModprobeIfAllowed(ModprobeIfAllowedRequest), @@ -338,310 +327,6 @@ pub struct ApplyHostGenerationHandoffResponse { pub summary: String, } -/// Request to launch the one-shot cutover runner. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct LaunchCutoverRunnerRequest { - /// Opaque operation identity used to derive runner-owned state. - pub operation_id: BundleOpId, - /// Required index of the single bootstrap fd attachment. - pub bootstrap_fd_index: u32, - /// Digest of the capability transferred over the bootstrap fd. The broker - /// binds this value to the operation before spawning the runner. - pub capability_digest: CanonicalAuditDigest, - /// Capability expiry copied from the single-use bootstrap. - pub expires_at_ms: u64, -} - -/// Response from the cutover runner launch. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct LaunchCutoverRunnerResponse { - /// Opaque operation identity. - pub operation_id: BundleOpId, - /// Child pid observed at launch. - pub pid: i32, - /// `/proc//stat` start-time captured by the broker. - pub start_time_ticks: u64, - /// Index of the returned pidfd, when the broker supplies one. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pidfd_index: Option, -} - -/// Closed transition vocabulary for cutover audit publication. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum CutoverAuditTransition { - /// A hold request was durably recorded. - HoldRequested, - /// A hold clear/resume was durably recorded. - HoldCleared, - /// A phase began after its journal record. - PhaseStarted, - /// A phase completed after its typed effect. - PhaseCompleted, - /// A typed effect began. - EffectStarted, - /// A typed effect completed. - EffectCompleted, - /// A terminal outcome was recorded. - Terminal, -} - -impl CutoverAuditTransition { - /// Return the stable audit disposition label. - pub const fn as_str(self) -> &'static str { - match self { - Self::HoldRequested => "hold-requested", - Self::HoldCleared => "hold-cleared", - Self::PhaseStarted => "phase-started", - Self::PhaseCompleted => "phase-completed", - Self::EffectStarted => "effect-started", - Self::EffectCompleted => "effect-completed", - Self::Terminal => "terminal", - } - } -} - -/// Request to publish one durable cutover audit transition. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverAuditRequest { - /// Operation identity bound by the runner capability. - pub operation_id: BundleOpId, - /// Current U3 phase number. - pub phase: u8, - /// Closed transition kind. - pub transition: CutoverAuditTransition, - /// Digest of the immutable operation request. - pub request_digest: CanonicalAuditDigest, - /// Digest of a bounded hold reason, when present. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason_digest: Option, -} - -/// Durable audit publication response. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverAuditResponse { - /// Stable record identity returned only after fsync and directory sync. - pub record_id: CanonicalAuditDigest, -} - -/// Closed authority carried by a cutover effect request. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum CutoverEffectAuthority { - /// Host-wide cutover authority. - Cutover, - /// Zone-scoped reset authority. - ResetZone, - /// Provider-scoped reset authority. - ResetProvider, - /// Guest-scoped reset authority. - ResetGuest, -} - -/// Closed effect vocabulary shared with the U3 allowlist. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum CutoverEffectKind { - HostDrain, - CutoverDisposition, - ResourceStoreCreate, - ProviderInstall, - ZoneActivation, - GuestActivation, - Verification, - CutoverFinalization, - ScopedZoneReset, - ScopedProviderReset, - ScopedGuestReset, - DestroyDurableVolume, - PreserveSource, - QuarantineDestination, - CutoverBroker, - ClosureActivation, - ApplyAdmission, -} - -/// Typed payloads for cutover effects that reuse existing broker operations. -/// -/// Every variant is itself a closed broker request. It carries no host path, -/// command, uid/gid, or free-form mutation text. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "kind", content = "payload")] -pub enum CutoverEffectPayload { - None, - ApplyAdmission(CutoverAdmissionRequest), - Storage(ReconcileStorageScopeRequest), - ZoneStore(OpenZoneStoreRequest), - StoreSync(StoreSyncRequest), - StoreVerify(StoreVerifyRequest), - Verification(CutoverVerificationRequest), - Activation(RunActivationRequest), - Systemd(Box), - Quarantine { - staged_id: BundleOpId, - source_id: BundleOpId, - marker_digest: CanonicalAuditDigest, - }, - Finalization { - artifacts: Vec, - disposition_digest: CanonicalAuditDigest, - consent_digest: CanonicalAuditDigest, - }, - DestroyDurableVolume { - storage_ref: BundleOpId, - marker_digest: CanonicalAuditDigest, - consent_digest: CanonicalAuditDigest, - }, -} - -/// Broker-owned phase-9 verification admission. The supplied Zone identities -/// are expected evidence bindings only; the broker returns the live -/// observations from its trusted bundle and host checks. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverVerificationRequest { - pub expected_zone_ids: Vec, -} - -/// One broker-owned Zone verification observation. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverZoneVerification { - pub zone_id: BundleOpId, - pub healthy: bool, -} - -/// Broker-owned phase-9 verification observations. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverVerificationResponse { - pub zones: Vec, - pub sources_preserved: bool, - pub identity_digests_match: bool, - pub candidate_current: bool, -} - -/// Broker-owned apply admission observations. These fields are never -/// caller-authored; the broker derives them from its trusted bundle and live -/// host ownership state. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverAdmissionResponse { - pub candidate_current: bool, - pub markers_valid: bool, - pub ownership_valid: bool, - pub predicates_hold: bool, -} - -/// Typed request for the broker-owned apply admission observation. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverAdmissionRequest { - /// Candidate artifact resolved and verified before host drain. - pub system_artifact_id: Option, - /// Preserved source artifact resolved and verified before host drain. - pub source_system_artifact_id: Option, -} - -/// Closed replay behavior for an operation-scoped effect. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum CutoverReplayClass { - Repeatable, - ReopenByJournaledIdentity, - QuarantineOnly, -} - -impl CutoverEffectAuthority { - /// Return whether this authority permits the effect kind. - pub const fn permits(self, effect: CutoverEffectKind) -> bool { - match self { - Self::Cutover => !matches!( - effect, - CutoverEffectKind::ScopedZoneReset - | CutoverEffectKind::ScopedProviderReset - | CutoverEffectKind::ScopedGuestReset - | CutoverEffectKind::DestroyDurableVolume - ), - Self::ResetZone => matches!( - effect, - CutoverEffectKind::ApplyAdmission - | CutoverEffectKind::ScopedZoneReset - | CutoverEffectKind::DestroyDurableVolume - | CutoverEffectKind::PreserveSource - | CutoverEffectKind::QuarantineDestination - | CutoverEffectKind::Verification - ), - Self::ResetProvider => matches!( - effect, - CutoverEffectKind::ApplyAdmission - | CutoverEffectKind::ScopedProviderReset - | CutoverEffectKind::DestroyDurableVolume - | CutoverEffectKind::PreserveSource - | CutoverEffectKind::QuarantineDestination - | CutoverEffectKind::Verification - ), - Self::ResetGuest => matches!( - effect, - CutoverEffectKind::ApplyAdmission - | CutoverEffectKind::ScopedGuestReset - | CutoverEffectKind::DestroyDurableVolume - | CutoverEffectKind::PreserveSource - | CutoverEffectKind::QuarantineDestination - | CutoverEffectKind::Verification - ), - } - } -} - -/// Typed operation-scoped effect request. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverEffectRequest { - pub operation_id: BundleOpId, - pub authority: CutoverEffectAuthority, - pub phase: u8, - pub effect_id: BundleOpId, - pub effect: CutoverEffectKind, - pub replay_class: CutoverReplayClass, - pub request_digest: CanonicalAuditDigest, - pub capability_digest: CanonicalAuditDigest, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub identity: Option, - /// Existing typed generation handoff used only by `ClosureActivation`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub handoff: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub payload: Option, -} - -/// Typed effect result. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum CutoverEffectOutcome { - Succeeded, - Failed, - Ambiguous, -} - -/// Typed operation-scoped effect response. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverEffectResponse { - pub outcome: CutoverEffectOutcome, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub identity: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub verification: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub admission: Option, - pub audit_record_id: CanonicalAuditDigest, -} - impl BrokerRequest { /// Stable operation name for audit records. /// @@ -652,9 +337,6 @@ impl BrokerRequest { pub fn op_name(&self) -> &'static str { match self { Self::ApplyHostGenerationHandoff(_) => "ApplyHostGenerationHandoff", - Self::LaunchCutoverRunner(_) => "LaunchCutoverRunner", - Self::CutoverAudit(_) => "CutoverAudit", - Self::CutoverEffect(_) => "CutoverEffect", Self::ApplyNftables(_) => "ApplyNftables", Self::ApplyNftablesProjection(_) => "ApplyNftablesProjection", Self::ApplyNmUnmanaged(_) => "ApplyNmUnmanaged", @@ -670,7 +352,6 @@ impl BrokerRequest { Self::DelegateCgroupV2(_) => "DelegateCgroupV2", Self::ExportBrokerAudit(_) => "ExportBrokerAudit", Self::Hello(_) => "Hello", - Self::GuestControlSign(_) => "GuestControlSign", Self::InjectSecretById(_) => "InjectSecretById", Self::LaunchMinijailChild(_) => "LaunchMinijailChild", Self::ModprobeIfAllowed(_) => "ModprobeIfAllowed", @@ -744,6 +425,15 @@ impl BrokerRequest { } } + /// Return whether this request is admitted by a fixed broker profile. + /// + /// The profile is selected by the broker process at startup. It is not + /// carried on the wire, so a request cannot switch or widen the active + /// authority domain. + pub fn allowed_by_profile(&self, profile: BrokerProfile) -> bool { + profile.allows_request(self) + } + /// Stable category label for the audit's "opaque_target_id" /// column. Mirrors `BootstrapCall::opaque_target_id` semantics: /// classify the kind of target without leaking caller-supplied @@ -752,7 +442,6 @@ impl BrokerRequest { pub fn opaque_target_id(&self) -> &'static str { match self { Self::Hello(_) => "daemon-handshake", - Self::GuestControlSign(_) => "guest-control-auth", Self::ValidateBundle => "bundle", Self::ResourceActivationAudit(_) => "resource-activation-audit", Self::ExportBrokerAudit(_) => "audit-log", @@ -859,10 +548,6 @@ impl BrokerRequest { request.scope_id.to_string(), format!("{}:{}", self.op_name(), request.scope_id), ), - Self::GuestControlSign(request) => ( - request.vm_id.to_string(), - format!("{}:{}", self.op_name(), request.vm_id), - ), Self::InjectSecretById(request) | Self::ReadSecretById(request) | Self::RotateSecretById(request) => ( @@ -1074,15 +759,6 @@ impl BrokerRequest { request.intent.target_generation ), ), - Self::LaunchCutoverRunner(request) => ( - request.operation_id.to_string(), - format!( - "{}:{}:{}", - self.op_name(), - request.operation_id, - request.bootstrap_fd_index - ), - ), Self::RunActivation(request) => ( request.vm.clone(), format!( @@ -1234,17 +910,12 @@ impl BrokerRequest { Self::ValidateBundle | Self::ExportBrokerAudit(_) | Self::Hello(_) - | Self::CutoverAudit(_) - | Self::CutoverEffect(_) | Self::PauseBroker | Self::PollChildReaped | Self::ResumeBroker => return None, }; Some(( - d2b_contracts_resource::v3::canonical_digest( - "d2b:broker-zone:v2", - scope.as_bytes(), - ), + d2b_contracts_resource::v3::canonical_digest("d2b:broker-zone:v2", scope.as_bytes()), d2b_contracts_resource::v3::canonical_digest( "d2b:broker-operation:v2", operation.as_bytes(), @@ -1261,8 +932,6 @@ impl BrokerRequest { Self::OpenPeerPidfdFromAcceptedSocket(_) | Self::ValidateBundle | Self::ExportBrokerAudit(_) - | Self::CutoverAudit(_) - | Self::CutoverEffect(_) | Self::Hello(_) | Self::PauseBroker | Self::PollChildReaped @@ -1271,6 +940,275 @@ impl BrokerRequest { } } +/// Fixed privileged-broker authority profiles. +/// +/// Host and Guest use the same wire and executable, but each process starts +/// with one closed operation catalog. The catalog is deliberately kept next +/// to the wire operation names so adding a request requires an explicit +/// profile decision. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BrokerProfile { + /// Host and realm authorities may use the complete host catalog. + Host, + /// Guest authorities may use only local process effects and read-only + /// broker lifecycle operations. + Guest, +} + +impl BrokerProfile { + /// Stable process-start profile label. + pub const fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::Guest => "guest", + } + } + + /// Closed Host operation catalog. + pub const fn host_operations() -> &'static [&'static str] { + HOST_OPERATION_CATALOG + } + + /// Closed Guest operation catalog. + pub const fn guest_operations() -> &'static [&'static str] { + GUEST_OPERATION_CATALOG + } + + /// Return the operation catalog for this profile. + pub const fn operations(self) -> &'static [&'static str] { + match self { + Self::Host => Self::host_operations(), + Self::Guest => Self::guest_operations(), + } + } + + /// Check the stable operation name against the profile catalog. + pub fn allows_operation(self, operation: &str) -> bool { + self.operations().contains(&operation) + } + + /// Check both the closed catalog and profile-specific target constraints. + pub fn allows_request(self, request: &BrokerRequest) -> bool { + if !self.allows_operation(request.op_name()) { + return false; + } + match self { + Self::Host => !Self::request_targets_guest(request), + Self::Guest => match request { + BrokerRequest::SpawnRunner(request) => { + request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && GUEST_LOCAL_RUNNER_ROLES.contains(&request.role) + && request.guest_execution.as_ref().is_some_and(GuestExecutionBinding::is_valid) + } + BrokerRequest::StartSystemdUnit(request) + | BrokerRequest::ObserveSystemdUnit(request) + | BrokerRequest::CheckSystemdUserManager(request) => request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::OpenPidfd(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::ObserveRunner(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::OpenSystemdUnitPidfd(request) => request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && request + .unit + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::StopSystemdUnit(request) => request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && request + .unit + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::SignalRunner(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::DeregisterRunnerPidfd(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + _ => true, + }, + } + } + + fn request_targets_guest(request: &BrokerRequest) -> bool { + match request { + BrokerRequest::SpawnRunner(request) => request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + || request.guest_execution.is_some(), + BrokerRequest::StartSystemdUnit(request) + | BrokerRequest::ObserveSystemdUnit(request) + | BrokerRequest::CheckSystemdUserManager(request) => request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + || request.guest_execution.is_some(), + BrokerRequest::OpenSystemdUnitPidfd(request) => { + request.unit.guest_execution.is_some() + || request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + } + BrokerRequest::StopSystemdUnit(request) => { + request.unit.guest_execution.is_some() + || request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + } + BrokerRequest::OpenPidfd(request) => request.guest_execution.is_some(), + BrokerRequest::ObserveRunner(request) => request.guest_execution.is_some(), + BrokerRequest::SignalRunner(request) => request.guest_execution.is_some(), + BrokerRequest::DeregisterRunnerPidfd(request) => request.guest_execution.is_some(), + _ => false, + } + } +} + +/// Every request currently defined by the broker wire. Host mode is closed +/// over this list rather than using an open-ended default. +pub const HOST_OPERATION_CATALOG: &[&str] = &[ + "ApplyHostGenerationHandoff", + "ApplyNftables", + "ApplyNftablesProjection", + "ApplyNmUnmanaged", + "ApplyRoute", + "ApplySysctl", + "BindUnixSocket", + "CreateOrReconcileUsersGroups", + "CreateBridge", + "DeleteBridge", + "CreatePersistentTap", + "DeletePersistentTap", + "CreateTapFd", + "DelegateCgroupV2", + "ExportBrokerAudit", + "Hello", + "InjectSecretById", + "LaunchMinijailChild", + "ModprobeIfAllowed", + "OpenCgroupDir", + "OpenDevice", + "OpenFuse", + "OpenHidrawSecurityKey", + "OpenKvm", + "QemuMediaEnroll", + "QemuMediaRefreshRegistry", + "QemuMediaBoot", + "QemuMediaSystemPowerdown", + "QemuMediaQueryStatus", + "QemuMediaQuit", + "QemuMediaAttach", + "QemuMediaDetach", + "OpenPidfd", + "OpenPeerPidfdFromAcceptedSocket", + "ObserveRunner", + "PipeWireAudio", + "StartSystemdUnit", + "CheckSystemdUserManager", + "ObserveSystemdUnit", + "OpenSystemdUnitPidfd", + "StopSystemdUnit", + "OpenZoneStore", + "OpenVhostNet", + "PauseBroker", + "PollChildReaped", + "PrepareRuntimeDir", + "PrepareStateDir", + "MigrateLegacySwtpmState", + "ReconcileStorageScope", + "ValidateLockSpec", + "PrepareStoreView", + "StoreSync", + "StoreVerify", + "ReadSecretById", + "ResumeBroker", + "RotateSecretById", + "RunHostInstall", + "RunMigrate", + "RunActivation", + "RunGc", + "RunKeysRotate", + "RunHostKeyTrust", + "RunRotateKnownHost", + "SetBridgePortFlags", + "SetSocketAcl", + "SetupMountNamespace", + "CgroupKill", + "SignalRunner", + "DeregisterRunnerPidfd", + "SpawnRunner", + "UpdateHostsFile", + "UsbipBind", + "UsbipBindFirewallRule", + "UsbipProxyReconcile", + "UsbipUnbind", + "UsbipExplicitBind", + "UsbipExplicitFirewallRule", + "ResourceActivationAudit", + "ValidateBundle", + "SeedDnsmasqLease", + "BindMountFromHardlinkFarm", + "OwnershipMatrixCheck", + "SshHostKeyPreflight", + "DiskInit", + "SecurityKeyOpenDevice", + "SecurityKeyApplyUdevRules", +]; + +/// Guest-local process and broker lifecycle effects. Host networking, +/// devices, storage, realm, and allocator operations are intentionally +/// absent from this catalog. +pub const GUEST_OPERATION_CATALOG: &[&str] = &[ + "Hello", + "ExportBrokerAudit", + "ValidateBundle", + "OpenPidfd", + "OpenPeerPidfdFromAcceptedSocket", + "ObserveRunner", + "StartSystemdUnit", + "CheckSystemdUserManager", + "ObserveSystemdUnit", + "OpenSystemdUnitPidfd", + "StopSystemdUnit", + "PollChildReaped", + "PrepareRuntimeDir", + "PrepareStateDir", + "SetupMountNamespace", + "CgroupKill", + "SignalRunner", + "DeregisterRunnerPidfd", + "SpawnRunner", +]; + /// Broker-side installer driver. The broker resolves the bundle's /// `installer:host` intent row (synthesised by /// `d2b_core::bundle_resolver` from the `host.json` + Nix-emitted @@ -1451,12 +1389,6 @@ pub struct HelloRequest { pub enum BrokerResponse { /// Result of one source-to-target generation handoff. ApplyHostGenerationHandoff(ApplyHostGenerationHandoffResponse), - /// Result of launching the operation-scoped cutover runner. - LaunchCutoverRunner(LaunchCutoverRunnerResponse), - /// Durable cutover audit publication result. - CutoverAudit(CutoverAuditResponse), - /// Typed operation-scoped effect result. - CutoverEffect(CutoverEffectResponse), Ack(AckResponse), CreatePersistentTap(TapReadyResponse), CreateTapFd(TapReadyResponse), @@ -1480,7 +1412,6 @@ pub enum BrokerResponse { /// capability-negotiate and the broker can audit the connection /// without a separate side-channel. Hello(HelloResponse), - GuestControlSign(GuestControlSignResponse), QemuMediaEnroll(QemuMediaEnrollResponse), QemuMediaRefreshRegistry(QemuMediaRefreshRegistryResponse), QemuMediaBoot(QemuMediaHotplugResponse), @@ -1824,114 +1755,6 @@ fn default_audit_export_limit() -> u32 { 256 } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestControlProofRole { - HostProof, - GuestProof, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestControlDirection { - HostToGuest, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestControlAuthPurpose { - GuestControlAuthV1, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct GuestBootIdWire(pub String); - -impl GuestBootIdWire { - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl JsonSchema for GuestBootIdWire { - fn schema_name() -> String { - "GuestBootIdWire".to_owned() - } - - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::Schema::Object(schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new( - schemars::schema::InstanceType::String, - ))), - string: Some(Box::new(schemars::schema::StringValidation { - min_length: Some(1), - max_length: Some(128), - ..Default::default() - })), - ..Default::default() - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestControlSignRequest { - pub vm_id: VmId, - pub role: GuestControlProofRole, - pub protocol_version: u32, - pub direction: GuestControlDirection, - pub purpose: GuestControlAuthPurpose, - pub guest_control_port: u32, - #[serde(default)] - pub peer_cid: Option, - #[schemars(length(min = 32, max = 32))] - pub host_nonce: Vec, - #[schemars(length(min = 32, max = 32))] - pub guest_nonce: Vec, - pub guest_boot_id: GuestBootIdWire, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub capabilities_hash: Option, - #[serde(default)] - pub tracing_span_id: Option, -} - -impl GuestControlSignRequest { - pub fn validate_shape(&self) -> Result<(), &'static str> { - if self.host_nonce.len() != AUTH_NONCE_LEN || self.guest_nonce.len() != AUTH_NONCE_LEN { - return Err("nonce-length"); - } - if self.guest_boot_id.as_str().is_empty() || self.guest_boot_id.as_str().len() > 128 { - return Err("guest-boot-id"); - } - match self.role { - GuestControlProofRole::HostProof if self.capabilities_hash.is_some() => { - Err("host-proof-capabilities-hash") - } - GuestControlProofRole::GuestProof => { - let Some(hash) = self.capabilities_hash.as_ref() else { - return Err("guest-proof-missing-capabilities-hash"); - }; - if hash.is_empty() || hash.len() > 128 { - return Err("capabilities-hash"); - } - Ok(()) - } - _ => Ok(()), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestControlSignResponse { - #[schemars(length(min = 32, max = 32))] - pub tag: Vec, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SecretByIdRequest { @@ -2165,6 +1988,35 @@ pub struct QemuMediaHotplugResponse { pub events: Vec, } +/// Exact authenticated binding for a Guest-local Process lifecycle. +/// +/// The values are commitments, not raw boot identifiers or transport +/// handles. A Guest broker requires this tuple for every target-local +/// Process operation and revalidates the boot commitment against its own +/// kernel before executing the request. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct GuestExecutionBinding { + pub target_uid: ResourceUid, + pub boot_identity_digest: [u8; 32], + pub session_generation: u64, + pub assignment_epoch: u64, + pub provider_generation: u64, + pub controller_generation: u64, +} + +impl GuestExecutionBinding { + /// Return whether all Guest execution commitments are populated. + pub fn is_valid(&self) -> bool { + self.target_uid.as_str().len() > 0 + && self.boot_identity_digest != [0; 32] + && self.session_generation > 0 + && self.assignment_epoch > 0 + && self.provider_generation > 0 + && self.controller_generation > 0 + } +} + /// OpenPidfd daemon-side reconcile-and-adopt support. The daemon's /// `d2bd::supervisor::state::reconcile_and_adopt` loop sends this /// request for every snapshot the classifier returned `Adopt` for. The @@ -2203,6 +2055,9 @@ pub struct OpenPidfdRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process adoption. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -2252,6 +2107,9 @@ pub struct ObserveRunnerRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process adoption. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -2354,6 +2212,9 @@ pub struct SystemdUnitIdentity { pub generation: u64, /// Content identity of the broker-resolved trusted bundle. pub bundle_content_identity: String, + /// Exact Guest target/session binding, when this is Guest-local. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, } /// Shared trusted request fields for systemd unit operations. @@ -2391,6 +2252,9 @@ pub struct SystemdUnitRequest { /// Canonical User resource bound to a user-domain launch. #[serde(default)] pub user_ref: Option, + /// Exact Guest target/session binding for target-local Process operations. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, /// Typed sandbox requirements enforced by the broker's systemd launch /// adapter. Legacy VM runner callers omit this field. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2975,6 +2839,9 @@ pub struct SignalRunnerRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process control. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -3011,6 +2878,9 @@ pub struct DeregisterRunnerPidfdRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process cleanup. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -3043,6 +2913,9 @@ pub enum RunnerRole { /// QEMU media runtime scaffold. The runtime Provider owns argv planning; /// the broker consumes the bundle-authoritative launch shape. QemuMedia, + /// Target-local one-shot NixOS activation runner. Guest mode may spawn + /// this role only from the bundle-authoritative process intent. + ActivationNixos, /// virtiofsd sidecar; one per `microvm.shares` row. The daemon/bundle /// provides argv from `nixos-modules/processes-json.nix`. Virtiofsd, @@ -3101,6 +2974,7 @@ impl RunnerRole { match self { Self::CloudHypervisor => "cloud-hypervisor", Self::QemuMedia => "qemu-media", + Self::ActivationNixos => "activation-nixos-runner", Self::Virtiofsd => "virtiofsd", Self::Swtpm => "swtpm", Self::SwtpmFlush => "swtpm-flush", @@ -3115,6 +2989,14 @@ impl RunnerRole { } } +/// Closed set of runner roles that a Guest broker may spawn locally. +/// +/// The current runner vocabulary contains mostly host-side VM, device, relay, +/// observability, and compositor helpers. The activation runner is the one +/// explicitly Guest-local role; future Guest Process roles must be added here +/// together with their signed bundle contract and identity fencing. +pub const GUEST_LOCAL_RUNNER_ROLES: &[RunnerRole] = &[RunnerRole::ActivationNixos]; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SpawnRunnerRequest { @@ -3141,6 +3023,9 @@ pub struct SpawnRunnerRequest { pub template_identity: Option<[u8; 32]>, #[serde(default, skip_serializing_if = "Option::is_none")] pub generation: Option, + /// Typed stdin input admitted only for the activation-nixos runner role. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub activation_input: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sandbox_plan: Option, /// Role selector - picks the argv generator the broker applies to @@ -3162,6 +3047,9 @@ pub struct SpawnRunnerRequest { /// Canonical User resource bound to a user-domain launch. #[serde(default)] pub user_ref: Option, + /// Exact Guest target/session binding for target-local Process launches. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, /// Optional vsock CID / TAP fd slot allocated by the daemon at /// host-prepare time. The broker validates each entry against the /// bundle row and refuses any unexpected allocation slot. None @@ -3232,6 +3120,8 @@ pub struct SpawnRunnerResponse { #[serde(default, skip_serializing_if = "Option::is_none")] pub user_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub provider_identity: Option<[u8; 32]>, #[serde(default, skip_serializing_if = "Option::is_none")] pub template_identity: Option<[u8; 32]>, @@ -3349,11 +3239,6 @@ pub enum BrokerCallerRole { RootUid { uid: u32, }, - /// Operation-scoped runner peer admitted only to closed cutover ops. - CutoverRunner { - operation_id: BundleOpId, - capability_digest: CanonicalAuditDigest, - }, #[default] NotAuthorized, } @@ -3363,17 +3248,11 @@ impl BrokerCallerRole { matches!(self, Self::AdminUid { .. }) } - /// Return whether this is the operation-scoped runner peer class. - pub fn is_cutover_runner(&self) -> bool { - matches!(self, Self::CutoverRunner { .. }) - } - pub fn for_display(&self) -> &'static str { match self { Self::AdminUid { .. } => "d2b-admin", Self::LauncherUid { .. } => "d2b-launcher", Self::RootUid { .. } => "RootUid", - Self::CutoverRunner { .. } => "d2b-cutover-runner", Self::NotAuthorized => "d2b-not-authorized", } } @@ -3636,17 +3515,6 @@ mod tests { BrokerCallerRole::AdminUid { uid: 1000 }.for_display(), "d2b-admin" ); - assert_eq!( - BrokerCallerRole::CutoverRunner { - operation_id: BundleOpId::new("op"), - capability_digest: CanonicalAuditDigest::parse( - "sha256:".to_owned() + &"a".repeat(64) - ) - .unwrap(), - } - .for_display(), - "d2b-cutover-runner" - ); assert_eq!( BrokerCallerRole::NotAuthorized.for_display(), "d2b-not-authorized" @@ -3659,13 +3527,6 @@ mod tests { BrokerCallerRole::AdminUid { uid: 1000 }, BrokerCallerRole::LauncherUid { uid: 1001 }, BrokerCallerRole::RootUid { uid: 0 }, - BrokerCallerRole::CutoverRunner { - operation_id: BundleOpId::new("op"), - capability_digest: CanonicalAuditDigest::parse( - "sha256:".to_owned() + &"a".repeat(64), - ) - .unwrap(), - }, BrokerCallerRole::NotAuthorized, ] { let json = serde_json::to_string(&role).unwrap(); @@ -4380,6 +4241,113 @@ mod tests { } } + fn spawn_runner_for_profile(role: RunnerRole, execution_ref: &str) -> BrokerRequest { + BrokerRequest::SpawnRunner(SpawnRunnerRequest { + vm_id: VmId::new("guest-vm"), + role_id: RoleId::new(role.as_str()), + resource_ref: None, + resource_uid: None, + bundle_content_identity: None, + provider_identity: None, + template_identity: None, + generation: None, + activation_input: None, + sandbox_plan: None, + role, + bundle_runner_intent_ref: BundleOpId::new("runner:test"), + execution_ref: Some(ResourceRef::parse(execution_ref).expect("valid execution ref")), + execution_domain: None, + user_ref: None, + guest_execution: None, + runtime_allocations: Vec::new(), + tracing_span_id: None, + workload_identity: None, + }) + } + + #[test] + fn profile_spawn_runner_role_matrix_is_closed() { + // No current RunnerRole is Guest-local. Guest Process resources use + // the dedicated local effect classes instead; a future runner role + // must be added to this allowlist deliberately. + let guest_local_roles: &[RunnerRole] = &[]; + let all_roles = [ + RunnerRole::CloudHypervisor, + RunnerRole::QemuMedia, + RunnerRole::Virtiofsd, + RunnerRole::Swtpm, + RunnerRole::SwtpmFlush, + RunnerRole::Gpu, + RunnerRole::Audio, + RunnerRole::Video, + RunnerRole::VsockRelay, + RunnerRole::Usbip, + RunnerRole::OtelHostBridge, + RunnerRole::WaylandProxy, + ]; + + for role in all_roles { + let expected_guest = guest_local_roles.contains(&role); + assert_eq!( + spawn_runner_for_profile(role, "Guest/guest-vm") + .allowed_by_profile(BrokerProfile::Guest), + expected_guest, + "Guest profile role {} must match the closed Guest-local allowlist", + role.as_str() + ); + assert!( + !spawn_runner_for_profile(role, "Host/host") + .allowed_by_profile(BrokerProfile::Guest), + "Guest profile must reject Host execution ref for {}", + role.as_str() + ); + assert!( + !spawn_runner_for_profile(role, "Guest/guest-vm") + .allowed_by_profile(BrokerProfile::Host), + "Host profile must reject Guest execution ref {}", + role.as_str() + ); + assert!( + spawn_runner_for_profile(role, "Host/host").allowed_by_profile(BrokerProfile::Host), + "Host profile behavior changed for Host execution ref {}", + role.as_str() + ); + } + } + + #[test] + fn guest_profile_requires_a_complete_target_execution_binding() { + let mut request = spawn_runner_for_profile( + RunnerRole::ActivationNixos, + "Guest/guest-vm", + ); + request = match request { + BrokerRequest::SpawnRunner(mut request) => { + request.guest_execution = Some(GuestExecutionBinding { + target_uid: ResourceUid::parse( + "123e4567-e89b-42d3-a456-426614174000", + ) + .expect("Guest UID"), + boot_identity_digest: [7; 32], + session_generation: 2, + assignment_epoch: 3, + provider_generation: 4, + controller_generation: 5, + }); + BrokerRequest::SpawnRunner(request) + } + _ => unreachable!("helper always builds SpawnRunner"), + }; + assert!(request.allowed_by_profile(BrokerProfile::Guest)); + assert!(!request.allowed_by_profile(BrokerProfile::Host)); + + let BrokerRequest::SpawnRunner(mut invalid) = request else { + unreachable!("helper always builds SpawnRunner"); + }; + invalid.guest_execution.as_mut().unwrap().assignment_epoch = 0; + assert!(!BrokerRequest::SpawnRunner(invalid).allowed_by_profile(BrokerProfile::Guest)); + } + #[test] fn spawn_runner_rejects_each_legacy_authority_field() { // argv, env, uid, gid, caps, seccomp_profile, @@ -4563,6 +4531,7 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, provider_identity: None, template_identity: None, generation: None, diff --git a/packages/d2b-contracts-broker/tests/cutover_wire.rs b/packages/d2b-contracts-broker/tests/cutover_wire.rs deleted file mode 100644 index d7f6bb3af9..0000000000 --- a/packages/d2b-contracts-broker/tests/cutover_wire.rs +++ /dev/null @@ -1,181 +0,0 @@ -use d2b_contracts::{FeatureFlag, KnownFeatureFlag, types::BundleOpId}; -use d2b_contracts_broker::broker_wire::{ - BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, CanonicalAuditDigest, - CutoverEffectAuthority, CutoverEffectKind, CutoverEffectPayload, CutoverEffectRequest, - CutoverReplayClass, LaunchCutoverRunnerRequest, ReconcileStorageScopeRequest, -}; -use d2b_contracts_control::public_wire::{ - HostCutoverOperation, HostCutoverRequest, HostCutoverResetScope, -}; -use d2b_contracts_resource::v3::ArtifactId; - -#[test] -fn public_cutover_request_round_trips_without_path_fields() { - let request = HostCutoverRequest { - operation: HostCutoverOperation::Apply, - operation_id: Some("op-wire".to_owned()), - candidate_id: Some("candidate-wire".to_owned()), - revision_plan_id: Some("plan-wire".to_owned()), - system_artifact_id: None, - source_system_artifact_id: None, - preview_digest: Some("sha256:".to_owned() + &"a".repeat(64)), - recovery_digest: Some("sha256:".to_owned() + &"b".repeat(64)), - operator_id: Some("uid-1000".to_owned()), - consent_digest: Some("sha256:".to_owned() + &"c".repeat(64)), - consent_json: None, - destructive_consent_digest: None, - destructive_consent_json: None, - destroy_durable_volumes: None, - recovery_attestation_json: None, - host_digest: None, - fresh_consent_digest: None, - reason: None, - reset_scope: None, - target: None, - zone: None, - }; - let value = serde_json::to_value(&request).expect("serialize request"); - assert!(!value.to_string().contains('/')); - let decoded: HostCutoverRequest = serde_json::from_value(value).expect("decode request"); - assert_eq!(decoded, request); -} - -#[test] -fn public_cutover_request_round_trips_system_artifact_identity() { - let request = HostCutoverRequest { - operation: HostCutoverOperation::Preview, - operation_id: Some("op-artifact-wire".to_owned()), - candidate_id: Some("candidate-artifact-wire".to_owned()), - revision_plan_id: Some("plan-artifact-wire".to_owned()), - system_artifact_id: Some("system-artifact-wire".to_owned()), - source_system_artifact_id: Some("source-artifact-wire".to_owned()), - preview_digest: None, - recovery_digest: None, - operator_id: None, - consent_digest: None, - consent_json: None, - destructive_consent_digest: None, - destructive_consent_json: None, - destroy_durable_volumes: None, - recovery_attestation_json: None, - host_digest: None, - fresh_consent_digest: None, - reason: None, - reset_scope: None, - target: None, - zone: None, - }; - let encoded = serde_json::to_string(&request).expect("serialize request"); - assert!(encoded.contains("\"systemArtifactId\":\"system-artifact-wire\"")); - assert!(encoded.contains("\"sourceSystemArtifactId\":\"source-artifact-wire\"")); - let decoded: HostCutoverRequest = serde_json::from_str(&encoded).expect("decode request"); - assert_eq!(decoded, request); -} - -#[test] -fn reset_authority_is_distinct_from_cutover_authority() { - let reset = HostCutoverRequest { - operation: HostCutoverOperation::Reset, - operation_id: Some("op-reset".to_owned()), - candidate_id: None, - revision_plan_id: None, - system_artifact_id: None, - source_system_artifact_id: None, - preview_digest: None, - recovery_digest: None, - operator_id: None, - consent_digest: None, - consent_json: None, - destructive_consent_digest: None, - destructive_consent_json: None, - destroy_durable_volumes: None, - recovery_attestation_json: None, - host_digest: None, - fresh_consent_digest: None, - reason: None, - reset_scope: Some(HostCutoverResetScope::Zone), - target: Some("zone-target".to_owned()), - zone: Some("zone-target".to_owned()), - }; - let encoded = serde_json::to_string(&reset).expect("serialize reset"); - assert!(encoded.contains("\"resetScope\":\"zone\"")); - assert!(!encoded.contains("\"operation\":\"apply\"")); -} - -#[test] -fn launch_runner_broker_request_has_no_spawn_runner_shape() { - let request = BrokerRequest::LaunchCutoverRunner(LaunchCutoverRunnerRequest { - operation_id: BundleOpId::new("op-wire"), - bootstrap_fd_index: 0, - capability_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)) - .expect("capability digest"), - expires_at_ms: 200, - }); - assert_eq!(request.op_name(), "LaunchCutoverRunner"); - assert!(!matches!(request, BrokerRequest::SpawnRunner(_))); - let envelope = BrokerRequestEnvelope { - request, - caller_role: BrokerCallerRole::AdminUid { uid: 1000 }, - test_peer_uid: None, - audit_join: None, - }; - let encoded = serde_json::to_string(&envelope).expect("serialize broker envelope"); - assert!(!encoded.contains("runnerPath")); - assert!(!encoded.contains("argv")); -} - -#[test] -fn cutover_runner_feature_is_explicitly_negotiated() { - let feature = FeatureFlag::new("cutover-runner-v1").expect("feature"); - assert_eq!(feature.known(), Some(KnownFeatureFlag::CutoverRunnerV1)); -} - -#[test] -fn cutover_effect_authorities_are_closed_and_non_overlapping() { - assert!(CutoverEffectAuthority::Cutover.permits(CutoverEffectKind::ApplyAdmission)); - assert!(CutoverEffectAuthority::Cutover.permits(CutoverEffectKind::ClosureActivation)); - assert!(!CutoverEffectAuthority::Cutover.permits(CutoverEffectKind::ScopedZoneReset)); - assert!(!CutoverEffectAuthority::Cutover.permits(CutoverEffectKind::DestroyDurableVolume)); - assert!(CutoverEffectAuthority::ResetZone.permits(CutoverEffectKind::ScopedZoneReset)); - assert!(CutoverEffectAuthority::ResetZone.permits(CutoverEffectKind::DestroyDurableVolume)); - assert!(!CutoverEffectAuthority::ResetZone.permits(CutoverEffectKind::HostDrain)); -} - -#[test] -fn cutover_effect_payload_reuses_opaque_storage_contract() { - let request = CutoverEffectRequest { - operation_id: BundleOpId::new("op-payload"), - authority: CutoverEffectAuthority::Cutover, - phase: 4, - effect_id: BundleOpId::new("effect-disposition"), - effect: CutoverEffectKind::CutoverDisposition, - replay_class: CutoverReplayClass::Repeatable, - request_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"a".repeat(64)) - .expect("request digest"), - capability_digest: CanonicalAuditDigest::parse("sha256:".to_owned() + &"b".repeat(64)) - .expect("capability digest"), - identity: None, - handoff: None, - payload: Some(CutoverEffectPayload::Storage( - ReconcileStorageScopeRequest { - storage_ref: BundleOpId::new("path:zone-store"), - apply: true, - tracing_span_id: None, - }, - )), - }; - let encoded = serde_json::to_string(&request).expect("serialize effect"); - assert!(!encoded.contains("/var/")); - let decoded: CutoverEffectRequest = serde_json::from_str(&encoded).expect("decode effect"); - assert_eq!(decoded, request); - - let admission = - CutoverEffectPayload::ApplyAdmission( - d2b_contracts_broker::broker_wire::CutoverAdmissionRequest { - system_artifact_id: Some(ArtifactId::parse("candidate").expect("candidate artifact")), - source_system_artifact_id: Some(ArtifactId::parse("source").expect("source artifact")), - }); - let encoded = serde_json::to_string(&admission).expect("serialize admission"); - let decoded: CutoverEffectPayload = serde_json::from_str(&encoded).expect("decode admission"); - assert_eq!(decoded, admission); -} diff --git a/packages/d2b-contracts-broker/tests/wire.rs b/packages/d2b-contracts-broker/tests/wire.rs index 0e8fa2b1db..852e119433 100644 --- a/packages/d2b-contracts-broker/tests/wire.rs +++ b/packages/d2b-contracts-broker/tests/wire.rs @@ -19,16 +19,12 @@ fn capabilities_keep_protocol_version_and_wire_tags() { "CreateOrReconcileUsersGroups", "CreatePersistentTap", "CreateTapFd", - "CutoverAudit", - "CutoverEffect", "DelegateCgroupV2", "DeleteBridge", "DeletePersistentTap", "ExportBrokerAudit", - "GuestControlSign", "Hello", "InjectSecretById", - "LaunchCutoverRunner", "LaunchMinijailChild", "MigrateLegacySwtpmState", "ModprobeIfAllowed", diff --git a/packages/d2b-contracts-control/BUILD.bazel b/packages/d2b-contracts-control/BUILD.bazel index 9d9a9fb0ae..ccfbcc0685 100644 --- a/packages/d2b-contracts-control/BUILD.bazel +++ b/packages/d2b-contracts-control/BUILD.bazel @@ -2,7 +2,7 @@ load("@crates//:defs.bzl", "all_crate_deps") load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test", "rust_doc_test") exports_files( - ["BUILD.bazel", "Cargo.toml", "guest_control.proto"], + ["BUILD.bazel", "Cargo.toml"], visibility = ["//visibility:public"], ) @@ -14,21 +14,6 @@ filegroup( visibility = ["//visibility:public"], ) -filegroup( - name = "guest_control_proto_source", - srcs = ["guest_control.proto"], - visibility = ["//visibility:public"], -) - -filegroup( - name = "guest_proto_generated", - srcs = [ - "src/generated/guest_control.rs", - "src/generated/mod.rs", - ], - visibility = ["//visibility:public"], -) - rust_library( name = "d2b_contracts_control", srcs = d2b_contracts_control_srcs, @@ -40,14 +25,6 @@ rust_library( ] + all_crate_deps(normal = True, cargo_only = True), ) -rust_test( - name = "guest_control", - srcs = ["tests/guest_control.rs"], - compile_data = ["Cargo.toml", "guest_control.proto"], - visibility = ["//visibility:public"], - deps = [":d2b_contracts_control"] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - rust_test( name = "public_wire", srcs = ["tests/public_wire.rs"], @@ -70,7 +47,7 @@ rust_doc_test( filegroup( name = "cargo_workspace_sources", - srcs = ["guest_control.proto"] + glob( + srcs = glob( [ "Cargo.lock", "Cargo.toml", diff --git a/packages/d2b-contracts-control/Cargo.toml b/packages/d2b-contracts-control/Cargo.toml index 8b2daf4eca..be8705e8ce 100644 --- a/packages/d2b-contracts-control/Cargo.toml +++ b/packages/d2b-contracts-control/Cargo.toml @@ -11,7 +11,6 @@ workspace = true [dependencies] d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } -protobuf = "3.7.2" schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/packages/d2b-contracts-control/guest_control.proto b/packages/d2b-contracts-control/guest_control.proto deleted file mode 100644 index 12982937b1..0000000000 --- a/packages/d2b-contracts-control/guest_control.proto +++ /dev/null @@ -1,872 +0,0 @@ -syntax = "proto3"; - -package d2b.guest.v1; - -// Guest-control ttRPC service. All exec I/O methods are unary chunked RPCs: -// callers use offsets, request ids, and bounded chunks instead of ttRPC -// streaming for stdin/stdout/stderr. -service GuestControl { - rpc Hello(HelloRequest) returns (HelloResponse); - rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse); - rpc Capabilities(CapabilitiesRequest) returns (CapabilitiesResponse); - rpc Health(HealthRequest) returns (HealthResponse); - - rpc ExecCreate(ExecCreateRequest) returns (ExecCreateResponse); - rpc ExecInspect(ExecInspectRequest) returns (ExecInspectResponse); - rpc ExecWait(ExecWaitRequest) returns (ExecWaitResponse); - rpc ExecLogs(ExecLogsRequest) returns (ExecLogsResponse); - rpc ExecList(ExecListRequest) returns (ExecListResponse); - - rpc WriteStdin(WriteStdinRequest) returns (WriteStdinResponse); - rpc ReadOutput(ReadOutputRequest) returns (ReadOutputResponse); - rpc CloseStdin(CloseStdinRequest) returns (CloseStdinResponse); - rpc TtyWinResize(TtyWinResizeRequest) returns (ControlAck); - rpc ExecSignal(ExecSignalRequest) returns (ControlAck); - rpc ExecCancel(ExecCancelRequest) returns (ControlAck); - - rpc ShellAttach(ShellAttachRequest) returns (ShellAttachResponse); - rpc ShellList(ShellListRequest) returns (ShellListResponse); - rpc ShellDetach(ShellDetachRequest) returns (ShellDetachResponse); - rpc ShellKill(ShellKillRequest) returns (ShellKillResponse); - rpc ShellCloseAttach(ShellCloseAttachRequest) returns (ShellDetachResponse); - rpc TerminalWriteStdin(TerminalWriteStdinRequest) returns (WriteStdinResponse); - rpc TerminalReadOutput(TerminalReadOutputRequest) returns (ReadOutputResponse); - rpc TerminalCloseStdin(TerminalCloseStdinRequest) returns (CloseStdinResponse); - rpc TerminalTtyWinResize(TerminalTtyWinResizeRequest) returns (ControlAck); - - // Framework op: read a host-declared, enum-keyed guest file in a - // single bounded shot. Auth-gated like the exec RPCs; available whenever - // guest-control is enabled (not gated on exec.enable). The request carries a - // closed file-id enum, never a free-form path. - rpc ReadGuestFile(ReadGuestFileRequest) returns (ReadGuestFileResponse); - - // Framework op: guest-side USBIP import lifecycle. The host daemon owns the - // host bind/unbind through the broker and calls this authenticated RPC for the - // guest attach/detach half; the CLI never SSHes into the guest for USBIP. - rpc UsbipImport(UsbipImportRequest) returns (UsbipImportResponse); - - // Framework op: side-effect-free guest-side USBIP import observation. Auth - // gated like UsbipImport; runs only the host-declared guest usbip binary. - rpc UsbipStatus(UsbipStatusRequest) returns (UsbipStatusResponse); - - // Framework op: guest-root system activation inside the VM. The host sends - // a store switch-to-configuration path and closed activation mode; guestd - // validates both and starts a detached transient unit in the guest systemd. - rpc ActivateSystemStart(GuestActivationStartRequest) returns (GuestActivationStartResponse); - - // Framework op: rejoinable status for an activation transient unit. Status is - // backed by guestd root-owned /run state and never returns stdout/stderr. - rpc ActivateSystemStatus(GuestActivationStatusRequest) returns (GuestActivationStatusResponse); - - // Audio ops: read/mutate per-VM audio policy from inside the guest. - // Auth-gated; guestd applies changes to the workload-user PipeWire session. - // Never invoked for qemu-media VMs (no guestd); ACA sandboxes route over the - // provider relay transport. - rpc AudioStatus(AudioStatusRequest) returns (AudioStatusResponse); - rpc AudioSet(AudioSetRequest) returns (AudioSetResponse); -} - -message RequestMetadata { - string vm_id = 1; - string request_id = 2; - uint32 protocol_version = 3; -} - -message ExecRequestMetadata { - RequestMetadata common = 1; - string exec_id = 2; - string guest_boot_id = 3; -} - -message TerminalRequestMetadata { - RequestMetadata common = 1; - string session_id = 2; - string guest_boot_id = 3; - TerminalKind kind = 4; -} - -message HelloRequest { - RequestMetadata metadata = 1; - bytes host_nonce = 2; - uint32 transcript_version = 3; -} - -message HelloResponse { - bytes guest_nonce = 1; - string guest_boot_id = 2; - uint32 protocol_version = 3; - reserved 4, 5; - reserved "capabilities_hash", "health"; -} - -message AuthenticateRequest { - RequestMetadata metadata = 1; - bytes host_nonce = 2; - bytes guest_nonce = 3; - string guest_boot_id = 4; - uint32 transcript_version = 5; - bytes host_auth_tag = 6; -} - -message AuthenticateResponse { - optional bytes guest_auth_tag = 1; - optional string capabilities_hash = 2; - HealthResponse health = 3; - CapabilitiesResponse capabilities = 4; - GuestControlError error = 5; -} - -message CapabilitiesRequest { - RequestMetadata metadata = 1; -} - -message CapabilitiesResponse { - uint32 protocol_version = 1; - repeated GuestCapability capabilities = 2; - GuestEffectiveLimits limits = 3; -} - -message HealthRequest { - RequestMetadata metadata = 1; -} - -message HealthResponse { - HealthOrigin origin = 1; - HealthState state = 2; - HealthReason reason = 3; - HealthRemediation remediation = 4; - uint32 protocol_version = 5; - repeated GuestCapability capabilities = 6; - repeated GuestSubsystem degraded_subsystems = 7; -} - -message GuestEffectiveLimits { - uint64 max_chunk_bytes = 1; - uint64 max_recv_message_bytes = 2; - uint64 decoded_write_stdin_bytes_per_connection = 3; - uint32 write_stdin_handlers_per_connection = 4; - uint32 stdin_queue_chunks_per_exec = 5; - uint64 stdout_live_buffer_bytes = 6; - uint64 stderr_live_buffer_bytes = 7; - uint64 detached_stdout_log_bytes = 8; - uint64 detached_stderr_log_bytes = 9; - uint64 long_poll_timeout_ms = 10; - uint64 slow_consumer_grace_ms = 11; - uint32 exec_sessions_per_vm = 12; - uint32 attached_sessions_per_vm = 13; - uint32 pending_read_output_waits_per_stream = 14; - uint32 pending_exec_waits_per_vm = 15; - uint32 rpc_rate_per_connection_per_second = 16; - uint32 rpc_rate_per_vm_burst = 17; - uint32 shell_sessions_per_vm = 18; - uint32 shell_attached_sessions_per_vm = 19; -} - -message ExecCreateRequest { - RequestMetadata metadata = 1; - repeated string argv = 2; - optional string user = 3; - optional string cwd = 4; - repeated EnvVar env = 5; - bool tty = 6; - bool stdin_open = 7; - bool detached = 8; - TerminalSize initial_terminal_size = 9; - OutputPolicy output_policy = 10; -} - -message EnvVar { - string key = 1; - string value = 2; -} - -message TerminalSize { - uint32 rows = 1; - uint32 cols = 2; -} - -message OutputPolicy { - uint64 max_chunk_bytes = 1; - uint64 max_stdout_log_bytes = 2; - uint64 max_stderr_log_bytes = 3; - uint64 slow_consumer_timeout_ms = 4; - uint64 wait_timeout_ms = 5; -} - -message ExecCreateResponse { - optional string exec_id = 1; - uint64 created_at_monotonic_ns = 2; - uint64 control_seq = 3; - uint64 stdout_cursor = 4; - uint64 stderr_cursor = 5; - GuestEffectiveLimits effective_limits = 6; - ExecState state = 7; - GuestControlError error = 8; -} - -message ExecInspectRequest { - ExecRequestMetadata metadata = 1; -} - -message ExecInspectResponse { - ExecState state = 1; - TerminalStatus visible_terminal_status = 2; - StdinState stdin_state = 3; - uint64 stdout_start_offset = 4; - uint64 stdout_end_offset = 5; - uint64 stderr_start_offset = 6; - uint64 stderr_end_offset = 7; - uint64 stdout_dropped_bytes = 8; - uint64 stderr_dropped_bytes = 9; - bool stdout_truncated_for_retention = 10; - bool stderr_truncated_for_retention = 11; - uint64 last_control_seq = 12; - uint64 state_generation = 13; - GuestControlError error = 14; -} - -message ExecWaitRequest { - ExecRequestMetadata metadata = 1; - uint64 timeout_ms = 2; - optional uint64 known_state_generation = 3; -} - -message ExecWaitResponse { - ExecState state = 1; - TerminalStatus visible_terminal_status = 2; - uint64 state_generation = 3; - uint64 stdout_start_offset = 4; - uint64 stdout_end_offset = 5; - uint64 stderr_start_offset = 6; - uint64 stderr_end_offset = 7; - bool timed_out = 8; - GuestControlError error = 9; -} - -message ExecLogsRequest { - ExecRequestMetadata metadata = 1; - OutputStream stream = 2; - uint64 offset = 3; - uint64 max_len = 4; -} - -message ExecLogsResponse { - OutputStream stream = 1; - uint64 offset = 2; - uint64 end_offset = 3; - bytes data = 4; - uint64 next_offset = 5; - bool eof = 6; - uint64 start_offset = 7; - uint64 dropped_bytes = 8; - bool truncated = 9; - GuestControlError error = 10; -} - -message ExecListRequest { - RequestMetadata metadata = 1; - string guest_boot_id = 2; -} - -message ExecListResponse { - repeated ExecListEntry entries = 1; - GuestControlError error = 2; -} - -message ExecListEntry { - string exec_id = 1; - uint32 slot = 2; - ExecState state = 3; - uint64 create_time_unix = 4; - string argv_sha256 = 5; - bool stdout_truncated = 6; - bool stderr_truncated = 7; - uint64 dropped_bytes = 8; -} - -message WriteStdinRequest { - ExecRequestMetadata metadata = 1; - uint64 offset = 2; - bytes data = 3; - bool close_after = 4; - optional uint64 client_deadline_ms = 5; -} - -message WriteStdinResponse { - uint64 accepted_offset = 1; - uint64 accepted_len = 2; - uint64 next_offset = 3; - StdinState stdin_state = 4; - uint64 blocked_ms = 5; - WriteDisposition disposition = 6; - GuestControlError error = 7; -} - -message ReadOutputRequest { - ExecRequestMetadata metadata = 1; - OutputStream stream = 2; - uint64 offset = 3; - uint64 max_len = 4; - bool wait = 5; - uint64 timeout_ms = 6; -} - -message ReadOutputResponse { - OutputStream stream = 1; - uint64 offset = 2; - uint64 end_offset = 3; - bytes data = 4; - uint64 next_offset = 5; - bool eof = 6; - uint64 start_offset = 7; - uint64 dropped_bytes = 8; - bool truncated = 9; - bool timed_out = 10; - GuestControlError error = 11; -} - -message CloseStdinRequest { - ExecRequestMetadata metadata = 1; - uint64 offset = 2; -} - -message CloseStdinResponse { - StdinState stdin_state = 1; - uint64 final_offset = 2; - WriteDisposition disposition = 3; - GuestControlError error = 4; -} - -message TtyWinResizeRequest { - ExecRequestMetadata metadata = 1; - uint64 control_seq = 2; - uint32 rows = 3; - uint32 cols = 4; -} - -message ExecSignalRequest { - ExecRequestMetadata metadata = 1; - uint64 control_seq = 2; - uint32 signal = 3; - SignalTarget target = 4; -} - -message ExecCancelRequest { - ExecRequestMetadata metadata = 1; - uint64 control_seq = 2; - ExecCancelReason reason = 3; -} - -message ControlAck { - uint64 control_seq = 1; - bool duplicate = 2; - GuestControlError error = 3; -} - -// Framework read of a host-declared guest file. The request is keyed by a -// closed enum (NOT a free-form path); guestd maps the key to the host-declared -// target path. The response is a single bounded payload; oversize files fail -// with FileTooLarge BEFORE any read/allocation. The guest-reported size/sha256 -// are diagnostics only - the host recomputes both from the received bytes. -message ReadGuestFileRequest { - RequestMetadata metadata = 1; - GuestFileId file_id = 2; -} - -message ReadGuestFileResponse { - GuestFileId file_id = 1; - uint64 size_bytes = 2; - bytes content = 3; - string sha256 = 4; - GuestControlError error = 5; -} - -message UsbipImportRequest { - RequestMetadata metadata = 1; - UsbipImportAction action = 2; - string host = 3; - string bus_id = 4; -} - -message UsbipImportResponse { - UsbipImportAction action = 1; - string bus_id = 2; - uint32 detached_ports = 3; - GuestControlError error = 4; -} - -message UsbipStatusRequest { - RequestMetadata metadata = 1; - optional string host = 2; - optional string bus_id = 3; -} - -message UsbipStatusResponse { - repeated UsbipStatusEntry imports = 1; - GuestControlError error = 2; -} - -message UsbipStatusEntry { - uint32 port = 1; - string host = 2; - uint32 tcp_port = 3; - string bus_id = 4; -} - -message GuestActivationStartRequest { - RequestMetadata metadata = 1; - string activation_id = 2; - string switch_script_path = 3; - GuestActivationMode mode = 4; - uint64 timeout_ms = 5; -} - -message GuestActivationStartResponse { - string activation_id = 1; - GuestActivationState state = 2; - GuestControlError error = 3; -} - -message GuestActivationStatusRequest { - RequestMetadata metadata = 1; - string activation_id = 2; -} - -message GuestActivationStatusResponse { - string activation_id = 1; - GuestActivationState state = 2; - optional int32 exit_code = 3; - optional uint32 signal = 4; - optional int32 status_code = 5; - GuestControlError error = 6; -} - -message ShellAttachRequest { - RequestMetadata metadata = 1; - optional string name = 2; - bool force = 3; - TerminalSize initial_terminal_size = 4; -} - -message ShellAttachResponse { - optional string session_id = 1; - string resolved_name = 2; - ShellState state = 3; - bool force_evicted = 4; - uint64 control_seq = 5; - uint64 output_cursor = 6; - GuestEffectiveLimits effective_limits = 7; - GuestControlError error = 8; -} - -message ShellListRequest { - RequestMetadata metadata = 1; -} - -message ShellListResponse { - string default_name = 1; - repeated ShellListEntry sessions = 2; - GuestControlError error = 3; -} - -message ShellListEntry { - string name = 1; - ShellState state = 2; - bool attached = 3; - bool is_default = 4; -} - -message ShellDetachRequest { - RequestMetadata metadata = 1; - optional string name = 2; -} - -message ShellKillRequest { - RequestMetadata metadata = 1; - string name = 2; -} - -message ShellCloseAttachRequest { - TerminalRequestMetadata metadata = 1; -} - -message ShellDetachResponse { - string resolved_name = 1; - bool detached = 2; - ShellCloseCause cause = 3; - GuestControlError error = 4; -} - -message ShellKillResponse { - string name = 1; - bool killed = 2; - ShellState state = 3; - GuestControlError error = 4; -} - -message TerminalWriteStdinRequest { - TerminalRequestMetadata metadata = 1; - uint64 offset = 2; - bytes data = 3; - bool close_after = 4; - optional uint64 client_deadline_ms = 5; -} - -message TerminalReadOutputRequest { - TerminalRequestMetadata metadata = 1; - OutputStream stream = 2; - uint64 offset = 3; - uint64 max_len = 4; - bool wait = 5; - uint64 timeout_ms = 6; -} - -message TerminalCloseStdinRequest { - TerminalRequestMetadata metadata = 1; - uint64 offset = 2; -} - -message TerminalTtyWinResizeRequest { - TerminalRequestMetadata metadata = 1; - uint64 control_seq = 2; - uint32 rows = 3; - uint32 cols = 4; -} - -message TerminalStatus { - oneof outcome { - int32 exit_code = 1; - uint32 signal = 2; - int32 status_code = 3; - GuestControlErrorKind error = 4; - } -} - -message GuestControlError { - GuestControlErrorKind kind = 1; - HealthRemediation remediation = 2; - optional uint64 retry_after_ms = 3; -} - -enum GuestCapability { - GUEST_CAPABILITY_UNSPECIFIED = 0; - GUEST_CAPABILITY_HEALTH = 1; - GUEST_CAPABILITY_CAPABILITIES = 2; - GUEST_CAPABILITY_EXEC_ATTACHED = 3; - GUEST_CAPABILITY_EXEC_DETACHED = 4; - GUEST_CAPABILITY_EXEC_TTY = 5; - GUEST_CAPABILITY_EXEC_LOGS = 6; - GUEST_CAPABILITY_TTY_RESIZE = 7; - GUEST_CAPABILITY_SIGNALS = 8; - GUEST_CAPABILITY_READ_GUEST_FILE = 9; - GUEST_CAPABILITY_USBIP_IMPORT = 10; - GUEST_CAPABILITY_SHELL_ATTACHED = 11; - GUEST_CAPABILITY_SHELL_MANAGEMENT = 12; - GUEST_CAPABILITY_SHELL_FORCE_ATTACH = 13; - GUEST_CAPABILITY_USBIP_STATUS = 14; - GUEST_CAPABILITY_SYSTEM_ACTIVATION = 15; - // Audio policy status query (read-only; workload-user PipeWire session). - GUEST_CAPABILITY_AUDIO_STATUS = 16; - // Audio policy mutation: mic/speaker grant + level/gain changes. - GUEST_CAPABILITY_AUDIO_SET = 17; -} - -enum GuestSubsystem { - GUEST_SUBSYSTEM_UNSPECIFIED = 0; - GUEST_SUBSYSTEM_GUESTD = 1; - GUEST_SUBSYSTEM_USERD = 2; - GUEST_SUBSYSTEM_EXEC = 3; - GUEST_SUBSYSTEM_LOG_STORAGE = 4; - GUEST_SUBSYSTEM_TOKEN = 5; - GUEST_SUBSYSTEM_VSOCK = 6; - GUEST_SUBSYSTEM_USBIP = 7; - GUEST_SUBSYSTEM_SHELL = 8; - GUEST_SUBSYSTEM_SHPOOL = 9; - GUEST_SUBSYSTEM_SYSTEM_ACTIVATION = 10; - GUEST_SUBSYSTEM_AUDIO = 11; -} - -enum TerminalKind { - TERMINAL_KIND_UNSPECIFIED = 0; - TERMINAL_KIND_EXEC = 1; - TERMINAL_KIND_SHELL = 2; -} - -enum GuestActivationMode { - GUEST_ACTIVATION_MODE_UNSPECIFIED = 0; - GUEST_ACTIVATION_MODE_SWITCH = 1; - GUEST_ACTIVATION_MODE_BOOT = 2; - GUEST_ACTIVATION_MODE_TEST = 3; - GUEST_ACTIVATION_MODE_DRY_ACTIVATE = 4; -} - -enum GuestActivationState { - GUEST_ACTIVATION_STATE_UNSPECIFIED = 0; - GUEST_ACTIVATION_STATE_RUNNING = 1; - GUEST_ACTIVATION_STATE_SUCCEEDED = 2; - GUEST_ACTIVATION_STATE_FAILED = 3; - GUEST_ACTIVATION_STATE_TIMED_OUT = 4; - GUEST_ACTIVATION_STATE_LOST = 5; -} - -enum ShellState { - SHELL_STATE_UNSPECIFIED = 0; - SHELL_STATE_ATTACHED = 1; - SHELL_STATE_DETACHED = 2; - SHELL_STATE_KILLED = 3; - SHELL_STATE_POOL_UNAVAILABLE = 4; - SHELL_STATE_FEATURE_DISABLED = 5; - SHELL_STATE_OUTPUT_GAP = 6; -} - -enum ShellCloseCause { - SHELL_CLOSE_CAUSE_UNSPECIFIED = 0; - SHELL_CLOSE_CAUSE_CLIENT_DETACH = 1; - SHELL_CLOSE_CAUSE_EVICTED_BY_FORCE = 2; - SHELL_CLOSE_CAUSE_EVICTED_BY_ADMIN_DETACH = 3; - SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN = 4; - SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE = 5; - SHELL_CLOSE_CAUSE_OUTPUT_GAP = 6; -} - -// Closed set of host-declared guest files readable via ReadGuestFile. -// The operation ships only GuestConfig (the in-guest editable config working -// copy). -enum GuestFileId { - GUEST_FILE_ID_UNSPECIFIED = 0; - GUEST_FILE_ID_GUEST_CONFIG = 1; -} - -enum UsbipImportAction { - USBIP_IMPORT_ACTION_UNSPECIFIED = 0; - USBIP_IMPORT_ACTION_ATTACH = 1; - USBIP_IMPORT_ACTION_DETACH = 2; -} - -enum HealthOrigin { - HEALTH_ORIGIN_UNSPECIFIED = 0; - HEALTH_ORIGIN_GUEST_REPORTED = 1; - HEALTH_ORIGIN_HOST_SYNTHESIZED = 2; -} - -enum HealthState { - HEALTH_STATE_UNSPECIFIED = 0; - HEALTH_STATE_HEALTHY = 1; - HEALTH_STATE_DEGRADED = 2; - HEALTH_STATE_UNAVAILABLE_OLD_GENERATION = 3; - HEALTH_STATE_LISTENER_ABSENT = 4; - HEALTH_STATE_TRANSPORT_UNREACHABLE = 5; - HEALTH_STATE_AUTH_FAILED = 6; - HEALTH_STATE_PROTOCOL_MISMATCH = 7; - HEALTH_STATE_STALE_SESSION = 8; -} - -enum HealthReason { - HEALTH_REASON_UNSPECIFIED = 0; - HEALTH_REASON_NONE = 1; - HEALTH_REASON_OLD_GENERATION = 2; - HEALTH_REASON_LISTENER_ABSENT = 3; - HEALTH_REASON_CONNECT_REFUSED = 4; - HEALTH_REASON_CONNECT_TIMEOUT = 5; - HEALTH_REASON_EOF_BEFORE_ACK = 6; - HEALTH_REASON_MALFORMED_ACK = 7; - HEALTH_REASON_ACK_TOO_LONG = 8; - HEALTH_REASON_TRANSPORT_IO = 9; - HEALTH_REASON_AUTH_TOKEN_REJECTED = 10; - HEALTH_REASON_PROTOCOL_VERSION_UNSUPPORTED = 11; - HEALTH_REASON_SESSION_GENERATION_MISMATCH = 12; - HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABLE = 13; - HEALTH_REASON_LOG_STORAGE_UNAVAILABLE = 14; - HEALTH_REASON_QUOTA_EXCEEDED = 15; - HEALTH_REASON_RATE_LIMITED = 16; - HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED = 17; -} - -enum HealthRemediation { - HEALTH_REMEDIATION_UNSPECIFIED = 0; - HEALTH_REMEDIATION_NONE = 1; - HEALTH_REMEDIATION_RETRY = 2; - HEALTH_REMEDIATION_RESTART_VM = 3; - HEALTH_REMEDIATION_UPGRADE_GUEST = 4; - HEALTH_REMEDIATION_CHECK_AUTH_TOKEN = 5; - HEALTH_REMEDIATION_CHECK_GUESTD_SERVICE = 6; - HEALTH_REMEDIATION_REDUCE_LOAD = 7; - HEALTH_REMEDIATION_INSPECT_GUEST_LOGS = 8; -} - -enum OutputStream { - OUTPUT_STREAM_UNSPECIFIED = 0; - OUTPUT_STREAM_STDOUT = 1; - OUTPUT_STREAM_STDERR = 2; -} - -enum WriteDisposition { - WRITE_DISPOSITION_UNSPECIFIED = 0; - WRITE_DISPOSITION_ACCEPTED = 1; - WRITE_DISPOSITION_DUPLICATE = 2; - WRITE_DISPOSITION_REJECTED = 3; -} - -enum ExecState { - EXEC_STATE_UNSPECIFIED = 0; - EXEC_STATE_CREATED = 1; - EXEC_STATE_RUNNING = 2; - EXEC_STATE_EXITED = 3; - EXEC_STATE_SIGNALED = 4; - EXEC_STATE_CANCELLED = 5; - EXEC_STATE_SLOW_CONSUMER_CANCELLED = 6; - EXEC_STATE_PROTOCOL_ERROR = 7; - EXEC_STATE_LOST_GUESTD = 8; - EXEC_STATE_REAPED = 9; -} - -enum StdinState { - STDIN_STATE_UNSPECIFIED = 0; - STDIN_STATE_OPEN = 1; - STDIN_STATE_CLOSING = 2; - STDIN_STATE_CLOSED = 3; - STDIN_STATE_CLOSED_BY_PROCESS = 4; - STDIN_STATE_REJECTED_NOT_INTERACTIVE = 5; -} - -enum SignalTarget { - SIGNAL_TARGET_UNSPECIFIED = 0; - SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP = 1; - SIGNAL_TARGET_PROCESS_TREE = 2; -} - -enum ExecCancelReason { - EXEC_CANCEL_REASON_UNSPECIFIED = 0; - EXEC_CANCEL_REASON_CLIENT_DISCONNECT = 1; - EXEC_CANCEL_REASON_USER_REQUESTED = 2; - EXEC_CANCEL_REASON_SLOW_CONSUMER = 3; - EXEC_CANCEL_REASON_PROTOCOL_ERROR = 4; -} - -enum GuestControlErrorKind { - GUEST_CONTROL_ERROR_KIND_UNSPECIFIED = 0; - GUEST_CONTROL_ERROR_KIND_PROTOCOL_ERROR = 1; - GUEST_CONTROL_ERROR_KIND_MAX_CHUNK_EXCEEDED = 2; - GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE = 3; - GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED = 4; - GUEST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN = 5; - GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED_BY_PROCESS = 6; - GUEST_CONTROL_ERROR_KIND_STDIN_OFFSET_MISMATCH = 7; - GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BUDGET_EXHAUSTED = 8; - GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED = 9; - GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE = 10; - GUEST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED = 11; - GUEST_CONTROL_ERROR_KIND_OUTPUT_LOST = 12; - GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UNAVAILABLE = 13; - GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED = 14; - GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED = 15; - GUEST_CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED = 16; - GUEST_CONTROL_ERROR_KIND_EXEC_NOT_FOUND = 17; - GUEST_CONTROL_ERROR_KIND_EXEC_ALREADY_EXITED = 18; - GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_DISABLED = 19; - GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED = 20; - GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED = 21; - GUEST_CONTROL_ERROR_KIND_CWD_INVALID = 22; - GUEST_CONTROL_ERROR_KIND_CWD_DENIED = 23; - GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_PATH_UNSAFE = 24; - GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QUOTA_EXCEEDED = 25; - GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_EXCEEDED = 26; - GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED = 27; - GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT = 28; - GUEST_CONTROL_ERROR_KIND_RATE_LIMITED = 29; - GUEST_CONTROL_ERROR_KIND_REQUEST_ID_CONFLICT = 30; - GUEST_CONTROL_ERROR_KIND_CONTROL_SEQ_MISMATCH = 31; - GUEST_CONTROL_ERROR_KIND_SLOW_CONSUMER_CANCELLED = 32; - GUEST_CONTROL_ERROR_KIND_STALE_SESSION = 33; - GUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATION = 34; - GUEST_CONTROL_ERROR_KIND_AUTH_FAILED = 35; - GUEST_CONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE = 36; - GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED = 37; - GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND = 38; - GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE = 39; - GUEST_CONTROL_ERROR_KIND_PATH_UNSAFE = 40; - GUEST_CONTROL_ERROR_KIND_READ_DENIED = 41; - GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM = 42; - GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE = 43; - GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED = 44; - GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_BUS_ID = 45; - GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST = 46; - GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED = 47; - GUEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME = 48; - GUEST_CONTROL_ERROR_KIND_SHELL_CAPACITY_EXCEEDED = 49; - GUEST_CONTROL_ERROR_KIND_SHELL_ATTACH_CAPACITY_EXCEEDED = 50; - GUEST_CONTROL_ERROR_KIND_SHELL_NOT_FOUND = 51; - GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED = 52; - GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE = 53; - GUEST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH = 54; - GUEST_CONTROL_ERROR_KIND_SHELL_OUTPUT_GAP = 55; - GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT = 56; - GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT = 57; - GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID = 58; - GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH = 59; - GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE = 60; - GUEST_CONTROL_ERROR_KIND_ACTIVATION_NOT_FOUND = 61; - GUEST_CONTROL_ERROR_KIND_ACTIVATION_STATUS_UNAVAILABLE = 62; - GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT = 63; - GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED = 64; - // Audio errors. - GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE = 65; - GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN = 66; - GUEST_CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE = 67; - GUEST_CONTROL_ERROR_KIND_AUDIO_ENFORCEMENT_FAILED = 68; -} - -// Audio channel selector. -enum AudioChannel { - AUDIO_CHANNEL_UNSPECIFIED = 0; - AUDIO_CHANNEL_MICROPHONE = 1; - AUDIO_CHANNEL_SPEAKER = 2; -} - -// Current state of one audio channel as seen by the guest PipeWire session. -message GuestAudioChannelState { - // True when the channel is muted (grant = off). - bool muted = 1; - // Current level 0-100; 0 when not configured or unavailable. - uint32 level = 2; - // True when the level value is meaningful (false → level is not available). - bool level_known = 3; -} - -// Request: query current guest audio policy state. -message AudioStatusRequest { - RequestMetadata metadata = 1; -} - -// Response: per-channel guest audio state. -message AudioStatusResponse { - GuestAudioChannelState microphone = 1; - GuestAudioChannelState speaker = 2; - GuestControlError error = 3; -} - -// Audio mutation kind. -enum AudioSetKind { - AUDIO_SET_KIND_UNSPECIFIED = 0; - // Turn a channel on (unmute) or off (mute). - AUDIO_SET_KIND_GRANT = 1; - // Adjust the channel level (volume/gain). - AUDIO_SET_KIND_LEVEL = 2; -} - -// Request: mutate one audio channel's grant or level in the guest session. -message AudioSetRequest { - RequestMetadata metadata = 1; - AudioChannel channel = 2; - AudioSetKind kind = 3; - // For GRANT: true = on, false = off. - bool grant_on = 4; - // For LEVEL: 0-100; ignored for GRANT. - uint32 level = 5; -} - -// Response: updated channel state after the mutation. -message AudioSetResponse { - GuestAudioChannelState state = 1; - GuestControlError error = 2; -} diff --git a/packages/d2b-contracts-control/src/cli_output.rs b/packages/d2b-contracts-control/src/cli_output.rs index a3faabadb1..9f95c56a7c 100644 --- a/packages/d2b-contracts-control/src/cli_output.rs +++ b/packages/d2b-contracts-control/src/cli_output.rs @@ -61,7 +61,7 @@ pub struct VmExecCreateOutputV1 { pub command: String, pub vm: String, pub exec_id: String, - pub state: crate::guest_wire::ExecState, + pub state: crate::public_wire::ExecState, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -86,7 +86,7 @@ pub struct VmExecListOutputV1 { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct VmExecListEntryOutputV1 { pub exec_id: String, - pub state: crate::guest_wire::ExecState, + pub state: crate::public_wire::ExecState, #[serde(skip_serializing_if = "Option::is_none")] pub exit_code: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -112,7 +112,7 @@ pub struct VmExecStatusOutputV1 { pub command: String, pub vm: String, pub exec_id: String, - pub state: crate::guest_wire::ExecState, + pub state: crate::public_wire::ExecState, #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -158,7 +158,7 @@ pub struct VmExecKillOutputV1 { pub vm: String, pub exec_id: String, pub result: crate::public_wire::ExecDetachedKillOutcome, - pub state: crate::guest_wire::ExecState, + pub state: crate::public_wire::ExecState, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] diff --git a/packages/d2b-contracts-control/src/generated/guest_control.rs b/packages/d2b-contracts-control/src/generated/guest_control.rs deleted file mode 100644 index 6e76dc56cc..0000000000 --- a/packages/d2b-contracts-control/src/generated/guest_control.rs +++ /dev/null @@ -1,15469 +0,0 @@ -// This file is generated by rust-protobuf 3.7.2. Do not edit -// .proto file is parsed by pure -// @generated - -#![allow(clippy::bool_comparison)] -#![allow(clippy::derivable_impls)] -#![allow(clippy::match_like_matches_macro)] -#![allow(clippy::match_ref_pats)] -#![allow(clippy::needless_borrow)] -#![allow(clippy::redundant_static_lifetimes)] -#![allow(clippy::vec_init_then_push)] - -#![allow(unused_attributes)] -#![cfg_attr(rustfmt, rustfmt::skip)] - -#![allow(dead_code)] -#![allow(missing_docs)] -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(trivial_casts)] -#![allow(unused_results)] -#![allow(unused_mut)] - -//! Generated file from `guest_control.proto` - -/// Generated files are compatible only with the same version -/// of protobuf runtime. -const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_3_7_2; - -// @@protoc_insertion_point(message:d2b.guest.v1.RequestMetadata) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct RequestMetadata { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.RequestMetadata.vm_id) - pub vm_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.RequestMetadata.request_id) - pub request_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.RequestMetadata.protocol_version) - pub protocol_version: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.RequestMetadata.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a RequestMetadata { - fn default() -> &'a RequestMetadata { - ::default_instance() - } -} - -impl RequestMetadata { - pub fn new() -> RequestMetadata { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "vm_id", - |m: &RequestMetadata| { &m.vm_id }, - |m: &mut RequestMetadata| { &mut m.vm_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "request_id", - |m: &RequestMetadata| { &m.request_id }, - |m: &mut RequestMetadata| { &mut m.request_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "protocol_version", - |m: &RequestMetadata| { &m.protocol_version }, - |m: &mut RequestMetadata| { &mut m.protocol_version }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "RequestMetadata", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for RequestMetadata { - const NAME: &'static str = "RequestMetadata"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.vm_id = is.read_string()?; - }, - 18 => { - self.request_id = is.read_string()?; - }, - 24 => { - self.protocol_version = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.vm_id.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.vm_id); - } - if !self.request_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.request_id); - } - if self.protocol_version != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.protocol_version); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.vm_id.is_empty() { - os.write_string(1, &self.vm_id)?; - } - if !self.request_id.is_empty() { - os.write_string(2, &self.request_id)?; - } - if self.protocol_version != 0 { - os.write_uint32(3, self.protocol_version)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> RequestMetadata { - RequestMetadata::new() - } - - fn clear(&mut self) { - self.vm_id.clear(); - self.request_id.clear(); - self.protocol_version = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static RequestMetadata { - static instance: RequestMetadata = RequestMetadata { - vm_id: ::std::string::String::new(), - request_id: ::std::string::String::new(), - protocol_version: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for RequestMetadata { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("RequestMetadata").unwrap()).clone() - } -} - -impl ::std::fmt::Display for RequestMetadata { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for RequestMetadata { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecRequestMetadata) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecRequestMetadata { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecRequestMetadata.common) - pub common: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecRequestMetadata.exec_id) - pub exec_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecRequestMetadata.guest_boot_id) - pub guest_boot_id: ::std::string::String, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecRequestMetadata.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecRequestMetadata { - fn default() -> &'a ExecRequestMetadata { - ::default_instance() - } -} - -impl ExecRequestMetadata { - pub fn new() -> ExecRequestMetadata { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "common", - |m: &ExecRequestMetadata| { &m.common }, - |m: &mut ExecRequestMetadata| { &mut m.common }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "exec_id", - |m: &ExecRequestMetadata| { &m.exec_id }, - |m: &mut ExecRequestMetadata| { &mut m.exec_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "guest_boot_id", - |m: &ExecRequestMetadata| { &m.guest_boot_id }, - |m: &mut ExecRequestMetadata| { &mut m.guest_boot_id }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecRequestMetadata", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecRequestMetadata { - const NAME: &'static str = "ExecRequestMetadata"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.common)?; - }, - 18 => { - self.exec_id = is.read_string()?; - }, - 26 => { - self.guest_boot_id = is.read_string()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.common.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.exec_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.exec_id); - } - if !self.guest_boot_id.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.guest_boot_id); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.common.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.exec_id.is_empty() { - os.write_string(2, &self.exec_id)?; - } - if !self.guest_boot_id.is_empty() { - os.write_string(3, &self.guest_boot_id)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecRequestMetadata { - ExecRequestMetadata::new() - } - - fn clear(&mut self) { - self.common.clear(); - self.exec_id.clear(); - self.guest_boot_id.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecRequestMetadata { - static instance: ExecRequestMetadata = ExecRequestMetadata { - common: ::protobuf::MessageField::none(), - exec_id: ::std::string::String::new(), - guest_boot_id: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecRequestMetadata { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecRequestMetadata").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecRequestMetadata { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecRequestMetadata { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TerminalRequestMetadata) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TerminalRequestMetadata { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalRequestMetadata.common) - pub common: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalRequestMetadata.session_id) - pub session_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalRequestMetadata.guest_boot_id) - pub guest_boot_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalRequestMetadata.kind) - pub kind: ::protobuf::EnumOrUnknown, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TerminalRequestMetadata.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TerminalRequestMetadata { - fn default() -> &'a TerminalRequestMetadata { - ::default_instance() - } -} - -impl TerminalRequestMetadata { - pub fn new() -> TerminalRequestMetadata { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "common", - |m: &TerminalRequestMetadata| { &m.common }, - |m: &mut TerminalRequestMetadata| { &mut m.common }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "session_id", - |m: &TerminalRequestMetadata| { &m.session_id }, - |m: &mut TerminalRequestMetadata| { &mut m.session_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "guest_boot_id", - |m: &TerminalRequestMetadata| { &m.guest_boot_id }, - |m: &mut TerminalRequestMetadata| { &mut m.guest_boot_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "kind", - |m: &TerminalRequestMetadata| { &m.kind }, - |m: &mut TerminalRequestMetadata| { &mut m.kind }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TerminalRequestMetadata", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TerminalRequestMetadata { - const NAME: &'static str = "TerminalRequestMetadata"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.common)?; - }, - 18 => { - self.session_id = is.read_string()?; - }, - 26 => { - self.guest_boot_id = is.read_string()?; - }, - 32 => { - self.kind = is.read_enum_or_unknown()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.common.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.session_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.session_id); - } - if !self.guest_boot_id.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.guest_boot_id); - } - if self.kind != ::protobuf::EnumOrUnknown::new(TerminalKind::TERMINAL_KIND_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(4, self.kind.value()); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.common.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.session_id.is_empty() { - os.write_string(2, &self.session_id)?; - } - if !self.guest_boot_id.is_empty() { - os.write_string(3, &self.guest_boot_id)?; - } - if self.kind != ::protobuf::EnumOrUnknown::new(TerminalKind::TERMINAL_KIND_UNSPECIFIED) { - os.write_enum(4, ::protobuf::EnumOrUnknown::value(&self.kind))?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TerminalRequestMetadata { - TerminalRequestMetadata::new() - } - - fn clear(&mut self) { - self.common.clear(); - self.session_id.clear(); - self.guest_boot_id.clear(); - self.kind = ::protobuf::EnumOrUnknown::new(TerminalKind::TERMINAL_KIND_UNSPECIFIED); - self.special_fields.clear(); - } - - fn default_instance() -> &'static TerminalRequestMetadata { - static instance: TerminalRequestMetadata = TerminalRequestMetadata { - common: ::protobuf::MessageField::none(), - session_id: ::std::string::String::new(), - guest_boot_id: ::std::string::String::new(), - kind: ::protobuf::EnumOrUnknown::from_i32(0), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TerminalRequestMetadata { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TerminalRequestMetadata").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TerminalRequestMetadata { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TerminalRequestMetadata { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.HelloRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct HelloRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.HelloRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.HelloRequest.host_nonce) - pub host_nonce: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.HelloRequest.transcript_version) - pub transcript_version: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.HelloRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a HelloRequest { - fn default() -> &'a HelloRequest { - ::default_instance() - } -} - -impl HelloRequest { - pub fn new() -> HelloRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &HelloRequest| { &m.metadata }, - |m: &mut HelloRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "host_nonce", - |m: &HelloRequest| { &m.host_nonce }, - |m: &mut HelloRequest| { &mut m.host_nonce }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "transcript_version", - |m: &HelloRequest| { &m.transcript_version }, - |m: &mut HelloRequest| { &mut m.transcript_version }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "HelloRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for HelloRequest { - const NAME: &'static str = "HelloRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.host_nonce = is.read_bytes()?; - }, - 24 => { - self.transcript_version = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.host_nonce.is_empty() { - my_size += ::protobuf::rt::bytes_size(2, &self.host_nonce); - } - if self.transcript_version != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.transcript_version); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.host_nonce.is_empty() { - os.write_bytes(2, &self.host_nonce)?; - } - if self.transcript_version != 0 { - os.write_uint32(3, self.transcript_version)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> HelloRequest { - HelloRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.host_nonce.clear(); - self.transcript_version = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static HelloRequest { - static instance: HelloRequest = HelloRequest { - metadata: ::protobuf::MessageField::none(), - host_nonce: ::std::vec::Vec::new(), - transcript_version: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for HelloRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("HelloRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for HelloRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for HelloRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.HelloResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct HelloResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.HelloResponse.guest_nonce) - pub guest_nonce: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.HelloResponse.guest_boot_id) - pub guest_boot_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.HelloResponse.protocol_version) - pub protocol_version: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.HelloResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a HelloResponse { - fn default() -> &'a HelloResponse { - ::default_instance() - } -} - -impl HelloResponse { - pub fn new() -> HelloResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "guest_nonce", - |m: &HelloResponse| { &m.guest_nonce }, - |m: &mut HelloResponse| { &mut m.guest_nonce }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "guest_boot_id", - |m: &HelloResponse| { &m.guest_boot_id }, - |m: &mut HelloResponse| { &mut m.guest_boot_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "protocol_version", - |m: &HelloResponse| { &m.protocol_version }, - |m: &mut HelloResponse| { &mut m.protocol_version }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "HelloResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for HelloResponse { - const NAME: &'static str = "HelloResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.guest_nonce = is.read_bytes()?; - }, - 18 => { - self.guest_boot_id = is.read_string()?; - }, - 24 => { - self.protocol_version = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.guest_nonce.is_empty() { - my_size += ::protobuf::rt::bytes_size(1, &self.guest_nonce); - } - if !self.guest_boot_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.guest_boot_id); - } - if self.protocol_version != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.protocol_version); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.guest_nonce.is_empty() { - os.write_bytes(1, &self.guest_nonce)?; - } - if !self.guest_boot_id.is_empty() { - os.write_string(2, &self.guest_boot_id)?; - } - if self.protocol_version != 0 { - os.write_uint32(3, self.protocol_version)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> HelloResponse { - HelloResponse::new() - } - - fn clear(&mut self) { - self.guest_nonce.clear(); - self.guest_boot_id.clear(); - self.protocol_version = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static HelloResponse { - static instance: HelloResponse = HelloResponse { - guest_nonce: ::std::vec::Vec::new(), - guest_boot_id: ::std::string::String::new(), - protocol_version: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for HelloResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("HelloResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for HelloResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for HelloResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.AuthenticateRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct AuthenticateRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateRequest.host_nonce) - pub host_nonce: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateRequest.guest_nonce) - pub guest_nonce: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateRequest.guest_boot_id) - pub guest_boot_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateRequest.transcript_version) - pub transcript_version: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateRequest.host_auth_tag) - pub host_auth_tag: ::std::vec::Vec, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.AuthenticateRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a AuthenticateRequest { - fn default() -> &'a AuthenticateRequest { - ::default_instance() - } -} - -impl AuthenticateRequest { - pub fn new() -> AuthenticateRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(6); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &AuthenticateRequest| { &m.metadata }, - |m: &mut AuthenticateRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "host_nonce", - |m: &AuthenticateRequest| { &m.host_nonce }, - |m: &mut AuthenticateRequest| { &mut m.host_nonce }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "guest_nonce", - |m: &AuthenticateRequest| { &m.guest_nonce }, - |m: &mut AuthenticateRequest| { &mut m.guest_nonce }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "guest_boot_id", - |m: &AuthenticateRequest| { &m.guest_boot_id }, - |m: &mut AuthenticateRequest| { &mut m.guest_boot_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "transcript_version", - |m: &AuthenticateRequest| { &m.transcript_version }, - |m: &mut AuthenticateRequest| { &mut m.transcript_version }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "host_auth_tag", - |m: &AuthenticateRequest| { &m.host_auth_tag }, - |m: &mut AuthenticateRequest| { &mut m.host_auth_tag }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "AuthenticateRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for AuthenticateRequest { - const NAME: &'static str = "AuthenticateRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.host_nonce = is.read_bytes()?; - }, - 26 => { - self.guest_nonce = is.read_bytes()?; - }, - 34 => { - self.guest_boot_id = is.read_string()?; - }, - 40 => { - self.transcript_version = is.read_uint32()?; - }, - 50 => { - self.host_auth_tag = is.read_bytes()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.host_nonce.is_empty() { - my_size += ::protobuf::rt::bytes_size(2, &self.host_nonce); - } - if !self.guest_nonce.is_empty() { - my_size += ::protobuf::rt::bytes_size(3, &self.guest_nonce); - } - if !self.guest_boot_id.is_empty() { - my_size += ::protobuf::rt::string_size(4, &self.guest_boot_id); - } - if self.transcript_version != 0 { - my_size += ::protobuf::rt::uint32_size(5, self.transcript_version); - } - if !self.host_auth_tag.is_empty() { - my_size += ::protobuf::rt::bytes_size(6, &self.host_auth_tag); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.host_nonce.is_empty() { - os.write_bytes(2, &self.host_nonce)?; - } - if !self.guest_nonce.is_empty() { - os.write_bytes(3, &self.guest_nonce)?; - } - if !self.guest_boot_id.is_empty() { - os.write_string(4, &self.guest_boot_id)?; - } - if self.transcript_version != 0 { - os.write_uint32(5, self.transcript_version)?; - } - if !self.host_auth_tag.is_empty() { - os.write_bytes(6, &self.host_auth_tag)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> AuthenticateRequest { - AuthenticateRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.host_nonce.clear(); - self.guest_nonce.clear(); - self.guest_boot_id.clear(); - self.transcript_version = 0; - self.host_auth_tag.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static AuthenticateRequest { - static instance: AuthenticateRequest = AuthenticateRequest { - metadata: ::protobuf::MessageField::none(), - host_nonce: ::std::vec::Vec::new(), - guest_nonce: ::std::vec::Vec::new(), - guest_boot_id: ::std::string::String::new(), - transcript_version: 0, - host_auth_tag: ::std::vec::Vec::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for AuthenticateRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("AuthenticateRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for AuthenticateRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for AuthenticateRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.AuthenticateResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct AuthenticateResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateResponse.guest_auth_tag) - pub guest_auth_tag: ::std::option::Option<::std::vec::Vec>, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateResponse.capabilities_hash) - pub capabilities_hash: ::std::option::Option<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateResponse.health) - pub health: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateResponse.capabilities) - pub capabilities: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.AuthenticateResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.AuthenticateResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a AuthenticateResponse { - fn default() -> &'a AuthenticateResponse { - ::default_instance() - } -} - -impl AuthenticateResponse { - pub fn new() -> AuthenticateResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(5); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "guest_auth_tag", - |m: &AuthenticateResponse| { &m.guest_auth_tag }, - |m: &mut AuthenticateResponse| { &mut m.guest_auth_tag }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "capabilities_hash", - |m: &AuthenticateResponse| { &m.capabilities_hash }, - |m: &mut AuthenticateResponse| { &mut m.capabilities_hash }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, HealthResponse>( - "health", - |m: &AuthenticateResponse| { &m.health }, - |m: &mut AuthenticateResponse| { &mut m.health }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, CapabilitiesResponse>( - "capabilities", - |m: &AuthenticateResponse| { &m.capabilities }, - |m: &mut AuthenticateResponse| { &mut m.capabilities }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &AuthenticateResponse| { &m.error }, - |m: &mut AuthenticateResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "AuthenticateResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for AuthenticateResponse { - const NAME: &'static str = "AuthenticateResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.guest_auth_tag = ::std::option::Option::Some(is.read_bytes()?); - }, - 18 => { - self.capabilities_hash = ::std::option::Option::Some(is.read_string()?); - }, - 26 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.health)?; - }, - 34 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.capabilities)?; - }, - 42 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.guest_auth_tag.as_ref() { - my_size += ::protobuf::rt::bytes_size(1, &v); - } - if let Some(v) = self.capabilities_hash.as_ref() { - my_size += ::protobuf::rt::string_size(2, &v); - } - if let Some(v) = self.health.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.capabilities.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.guest_auth_tag.as_ref() { - os.write_bytes(1, v)?; - } - if let Some(v) = self.capabilities_hash.as_ref() { - os.write_string(2, v)?; - } - if let Some(v) = self.health.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; - } - if let Some(v) = self.capabilities.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(5, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> AuthenticateResponse { - AuthenticateResponse::new() - } - - fn clear(&mut self) { - self.guest_auth_tag = ::std::option::Option::None; - self.capabilities_hash = ::std::option::Option::None; - self.health.clear(); - self.capabilities.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static AuthenticateResponse { - static instance: AuthenticateResponse = AuthenticateResponse { - guest_auth_tag: ::std::option::Option::None, - capabilities_hash: ::std::option::Option::None, - health: ::protobuf::MessageField::none(), - capabilities: ::protobuf::MessageField::none(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for AuthenticateResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("AuthenticateResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for AuthenticateResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for AuthenticateResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.CapabilitiesRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct CapabilitiesRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.CapabilitiesRequest.metadata) - pub metadata: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.CapabilitiesRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a CapabilitiesRequest { - fn default() -> &'a CapabilitiesRequest { - ::default_instance() - } -} - -impl CapabilitiesRequest { - pub fn new() -> CapabilitiesRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(1); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &CapabilitiesRequest| { &m.metadata }, - |m: &mut CapabilitiesRequest| { &mut m.metadata }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "CapabilitiesRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for CapabilitiesRequest { - const NAME: &'static str = "CapabilitiesRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> CapabilitiesRequest { - CapabilitiesRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static CapabilitiesRequest { - static instance: CapabilitiesRequest = CapabilitiesRequest { - metadata: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for CapabilitiesRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("CapabilitiesRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for CapabilitiesRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for CapabilitiesRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.CapabilitiesResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct CapabilitiesResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.CapabilitiesResponse.protocol_version) - pub protocol_version: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.CapabilitiesResponse.capabilities) - pub capabilities: ::std::vec::Vec<::protobuf::EnumOrUnknown>, - // @@protoc_insertion_point(field:d2b.guest.v1.CapabilitiesResponse.limits) - pub limits: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.CapabilitiesResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a CapabilitiesResponse { - fn default() -> &'a CapabilitiesResponse { - ::default_instance() - } -} - -impl CapabilitiesResponse { - pub fn new() -> CapabilitiesResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "protocol_version", - |m: &CapabilitiesResponse| { &m.protocol_version }, - |m: &mut CapabilitiesResponse| { &mut m.protocol_version }, - )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "capabilities", - |m: &CapabilitiesResponse| { &m.capabilities }, - |m: &mut CapabilitiesResponse| { &mut m.capabilities }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestEffectiveLimits>( - "limits", - |m: &CapabilitiesResponse| { &m.limits }, - |m: &mut CapabilitiesResponse| { &mut m.limits }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "CapabilitiesResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for CapabilitiesResponse { - const NAME: &'static str = "CapabilitiesResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.protocol_version = is.read_uint32()?; - }, - 16 => { - self.capabilities.push(is.read_enum_or_unknown()?); - }, - 18 => { - ::protobuf::rt::read_repeated_packed_enum_or_unknown_into(is, &mut self.capabilities)? - }, - 26 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.limits)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.protocol_version != 0 { - my_size += ::protobuf::rt::uint32_size(1, self.protocol_version); - } - my_size += ::protobuf::rt::vec_packed_enum_or_unknown_size(2, &self.capabilities); - if let Some(v) = self.limits.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.protocol_version != 0 { - os.write_uint32(1, self.protocol_version)?; - } - os.write_repeated_packed_enum_or_unknown(2, &self.capabilities)?; - if let Some(v) = self.limits.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> CapabilitiesResponse { - CapabilitiesResponse::new() - } - - fn clear(&mut self) { - self.protocol_version = 0; - self.capabilities.clear(); - self.limits.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static CapabilitiesResponse { - static instance: CapabilitiesResponse = CapabilitiesResponse { - protocol_version: 0, - capabilities: ::std::vec::Vec::new(), - limits: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for CapabilitiesResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("CapabilitiesResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for CapabilitiesResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for CapabilitiesResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.HealthRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct HealthRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.HealthRequest.metadata) - pub metadata: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.HealthRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a HealthRequest { - fn default() -> &'a HealthRequest { - ::default_instance() - } -} - -impl HealthRequest { - pub fn new() -> HealthRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(1); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &HealthRequest| { &m.metadata }, - |m: &mut HealthRequest| { &mut m.metadata }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "HealthRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for HealthRequest { - const NAME: &'static str = "HealthRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> HealthRequest { - HealthRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static HealthRequest { - static instance: HealthRequest = HealthRequest { - metadata: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for HealthRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("HealthRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for HealthRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for HealthRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.HealthResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct HealthResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.HealthResponse.origin) - pub origin: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.HealthResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.HealthResponse.reason) - pub reason: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.HealthResponse.remediation) - pub remediation: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.HealthResponse.protocol_version) - pub protocol_version: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.HealthResponse.capabilities) - pub capabilities: ::std::vec::Vec<::protobuf::EnumOrUnknown>, - // @@protoc_insertion_point(field:d2b.guest.v1.HealthResponse.degraded_subsystems) - pub degraded_subsystems: ::std::vec::Vec<::protobuf::EnumOrUnknown>, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.HealthResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a HealthResponse { - fn default() -> &'a HealthResponse { - ::default_instance() - } -} - -impl HealthResponse { - pub fn new() -> HealthResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(7); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "origin", - |m: &HealthResponse| { &m.origin }, - |m: &mut HealthResponse| { &mut m.origin }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &HealthResponse| { &m.state }, - |m: &mut HealthResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "reason", - |m: &HealthResponse| { &m.reason }, - |m: &mut HealthResponse| { &mut m.reason }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "remediation", - |m: &HealthResponse| { &m.remediation }, - |m: &mut HealthResponse| { &mut m.remediation }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "protocol_version", - |m: &HealthResponse| { &m.protocol_version }, - |m: &mut HealthResponse| { &mut m.protocol_version }, - )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "capabilities", - |m: &HealthResponse| { &m.capabilities }, - |m: &mut HealthResponse| { &mut m.capabilities }, - )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "degraded_subsystems", - |m: &HealthResponse| { &m.degraded_subsystems }, - |m: &mut HealthResponse| { &mut m.degraded_subsystems }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "HealthResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for HealthResponse { - const NAME: &'static str = "HealthResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.origin = is.read_enum_or_unknown()?; - }, - 16 => { - self.state = is.read_enum_or_unknown()?; - }, - 24 => { - self.reason = is.read_enum_or_unknown()?; - }, - 32 => { - self.remediation = is.read_enum_or_unknown()?; - }, - 40 => { - self.protocol_version = is.read_uint32()?; - }, - 48 => { - self.capabilities.push(is.read_enum_or_unknown()?); - }, - 50 => { - ::protobuf::rt::read_repeated_packed_enum_or_unknown_into(is, &mut self.capabilities)? - }, - 56 => { - self.degraded_subsystems.push(is.read_enum_or_unknown()?); - }, - 58 => { - ::protobuf::rt::read_repeated_packed_enum_or_unknown_into(is, &mut self.degraded_subsystems)? - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.origin != ::protobuf::EnumOrUnknown::new(HealthOrigin::HEALTH_ORIGIN_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.origin.value()); - } - if self.state != ::protobuf::EnumOrUnknown::new(HealthState::HEALTH_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.state.value()); - } - if self.reason != ::protobuf::EnumOrUnknown::new(HealthReason::HEALTH_REASON_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.reason.value()); - } - if self.remediation != ::protobuf::EnumOrUnknown::new(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(4, self.remediation.value()); - } - if self.protocol_version != 0 { - my_size += ::protobuf::rt::uint32_size(5, self.protocol_version); - } - my_size += ::protobuf::rt::vec_packed_enum_or_unknown_size(6, &self.capabilities); - my_size += ::protobuf::rt::vec_packed_enum_or_unknown_size(7, &self.degraded_subsystems); - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.origin != ::protobuf::EnumOrUnknown::new(HealthOrigin::HEALTH_ORIGIN_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.origin))?; - } - if self.state != ::protobuf::EnumOrUnknown::new(HealthState::HEALTH_STATE_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if self.reason != ::protobuf::EnumOrUnknown::new(HealthReason::HEALTH_REASON_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.reason))?; - } - if self.remediation != ::protobuf::EnumOrUnknown::new(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED) { - os.write_enum(4, ::protobuf::EnumOrUnknown::value(&self.remediation))?; - } - if self.protocol_version != 0 { - os.write_uint32(5, self.protocol_version)?; - } - os.write_repeated_packed_enum_or_unknown(6, &self.capabilities)?; - os.write_repeated_packed_enum_or_unknown(7, &self.degraded_subsystems)?; - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> HealthResponse { - HealthResponse::new() - } - - fn clear(&mut self) { - self.origin = ::protobuf::EnumOrUnknown::new(HealthOrigin::HEALTH_ORIGIN_UNSPECIFIED); - self.state = ::protobuf::EnumOrUnknown::new(HealthState::HEALTH_STATE_UNSPECIFIED); - self.reason = ::protobuf::EnumOrUnknown::new(HealthReason::HEALTH_REASON_UNSPECIFIED); - self.remediation = ::protobuf::EnumOrUnknown::new(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED); - self.protocol_version = 0; - self.capabilities.clear(); - self.degraded_subsystems.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static HealthResponse { - static instance: HealthResponse = HealthResponse { - origin: ::protobuf::EnumOrUnknown::from_i32(0), - state: ::protobuf::EnumOrUnknown::from_i32(0), - reason: ::protobuf::EnumOrUnknown::from_i32(0), - remediation: ::protobuf::EnumOrUnknown::from_i32(0), - protocol_version: 0, - capabilities: ::std::vec::Vec::new(), - degraded_subsystems: ::std::vec::Vec::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for HealthResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("HealthResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for HealthResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for HealthResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.GuestEffectiveLimits) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct GuestEffectiveLimits { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.max_chunk_bytes) - pub max_chunk_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.max_recv_message_bytes) - pub max_recv_message_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.decoded_write_stdin_bytes_per_connection) - pub decoded_write_stdin_bytes_per_connection: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.write_stdin_handlers_per_connection) - pub write_stdin_handlers_per_connection: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.stdin_queue_chunks_per_exec) - pub stdin_queue_chunks_per_exec: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.stdout_live_buffer_bytes) - pub stdout_live_buffer_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.stderr_live_buffer_bytes) - pub stderr_live_buffer_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.detached_stdout_log_bytes) - pub detached_stdout_log_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.detached_stderr_log_bytes) - pub detached_stderr_log_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.long_poll_timeout_ms) - pub long_poll_timeout_ms: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.slow_consumer_grace_ms) - pub slow_consumer_grace_ms: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.exec_sessions_per_vm) - pub exec_sessions_per_vm: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.attached_sessions_per_vm) - pub attached_sessions_per_vm: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.pending_read_output_waits_per_stream) - pub pending_read_output_waits_per_stream: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.pending_exec_waits_per_vm) - pub pending_exec_waits_per_vm: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.rpc_rate_per_connection_per_second) - pub rpc_rate_per_connection_per_second: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.rpc_rate_per_vm_burst) - pub rpc_rate_per_vm_burst: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.shell_sessions_per_vm) - pub shell_sessions_per_vm: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestEffectiveLimits.shell_attached_sessions_per_vm) - pub shell_attached_sessions_per_vm: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.GuestEffectiveLimits.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a GuestEffectiveLimits { - fn default() -> &'a GuestEffectiveLimits { - ::default_instance() - } -} - -impl GuestEffectiveLimits { - pub fn new() -> GuestEffectiveLimits { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(19); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_chunk_bytes", - |m: &GuestEffectiveLimits| { &m.max_chunk_bytes }, - |m: &mut GuestEffectiveLimits| { &mut m.max_chunk_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_recv_message_bytes", - |m: &GuestEffectiveLimits| { &m.max_recv_message_bytes }, - |m: &mut GuestEffectiveLimits| { &mut m.max_recv_message_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "decoded_write_stdin_bytes_per_connection", - |m: &GuestEffectiveLimits| { &m.decoded_write_stdin_bytes_per_connection }, - |m: &mut GuestEffectiveLimits| { &mut m.decoded_write_stdin_bytes_per_connection }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "write_stdin_handlers_per_connection", - |m: &GuestEffectiveLimits| { &m.write_stdin_handlers_per_connection }, - |m: &mut GuestEffectiveLimits| { &mut m.write_stdin_handlers_per_connection }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdin_queue_chunks_per_exec", - |m: &GuestEffectiveLimits| { &m.stdin_queue_chunks_per_exec }, - |m: &mut GuestEffectiveLimits| { &mut m.stdin_queue_chunks_per_exec }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_live_buffer_bytes", - |m: &GuestEffectiveLimits| { &m.stdout_live_buffer_bytes }, - |m: &mut GuestEffectiveLimits| { &mut m.stdout_live_buffer_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_live_buffer_bytes", - |m: &GuestEffectiveLimits| { &m.stderr_live_buffer_bytes }, - |m: &mut GuestEffectiveLimits| { &mut m.stderr_live_buffer_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "detached_stdout_log_bytes", - |m: &GuestEffectiveLimits| { &m.detached_stdout_log_bytes }, - |m: &mut GuestEffectiveLimits| { &mut m.detached_stdout_log_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "detached_stderr_log_bytes", - |m: &GuestEffectiveLimits| { &m.detached_stderr_log_bytes }, - |m: &mut GuestEffectiveLimits| { &mut m.detached_stderr_log_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "long_poll_timeout_ms", - |m: &GuestEffectiveLimits| { &m.long_poll_timeout_ms }, - |m: &mut GuestEffectiveLimits| { &mut m.long_poll_timeout_ms }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "slow_consumer_grace_ms", - |m: &GuestEffectiveLimits| { &m.slow_consumer_grace_ms }, - |m: &mut GuestEffectiveLimits| { &mut m.slow_consumer_grace_ms }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "exec_sessions_per_vm", - |m: &GuestEffectiveLimits| { &m.exec_sessions_per_vm }, - |m: &mut GuestEffectiveLimits| { &mut m.exec_sessions_per_vm }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "attached_sessions_per_vm", - |m: &GuestEffectiveLimits| { &m.attached_sessions_per_vm }, - |m: &mut GuestEffectiveLimits| { &mut m.attached_sessions_per_vm }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "pending_read_output_waits_per_stream", - |m: &GuestEffectiveLimits| { &m.pending_read_output_waits_per_stream }, - |m: &mut GuestEffectiveLimits| { &mut m.pending_read_output_waits_per_stream }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "pending_exec_waits_per_vm", - |m: &GuestEffectiveLimits| { &m.pending_exec_waits_per_vm }, - |m: &mut GuestEffectiveLimits| { &mut m.pending_exec_waits_per_vm }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "rpc_rate_per_connection_per_second", - |m: &GuestEffectiveLimits| { &m.rpc_rate_per_connection_per_second }, - |m: &mut GuestEffectiveLimits| { &mut m.rpc_rate_per_connection_per_second }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "rpc_rate_per_vm_burst", - |m: &GuestEffectiveLimits| { &m.rpc_rate_per_vm_burst }, - |m: &mut GuestEffectiveLimits| { &mut m.rpc_rate_per_vm_burst }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "shell_sessions_per_vm", - |m: &GuestEffectiveLimits| { &m.shell_sessions_per_vm }, - |m: &mut GuestEffectiveLimits| { &mut m.shell_sessions_per_vm }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "shell_attached_sessions_per_vm", - |m: &GuestEffectiveLimits| { &m.shell_attached_sessions_per_vm }, - |m: &mut GuestEffectiveLimits| { &mut m.shell_attached_sessions_per_vm }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "GuestEffectiveLimits", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for GuestEffectiveLimits { - const NAME: &'static str = "GuestEffectiveLimits"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.max_chunk_bytes = is.read_uint64()?; - }, - 16 => { - self.max_recv_message_bytes = is.read_uint64()?; - }, - 24 => { - self.decoded_write_stdin_bytes_per_connection = is.read_uint64()?; - }, - 32 => { - self.write_stdin_handlers_per_connection = is.read_uint32()?; - }, - 40 => { - self.stdin_queue_chunks_per_exec = is.read_uint32()?; - }, - 48 => { - self.stdout_live_buffer_bytes = is.read_uint64()?; - }, - 56 => { - self.stderr_live_buffer_bytes = is.read_uint64()?; - }, - 64 => { - self.detached_stdout_log_bytes = is.read_uint64()?; - }, - 72 => { - self.detached_stderr_log_bytes = is.read_uint64()?; - }, - 80 => { - self.long_poll_timeout_ms = is.read_uint64()?; - }, - 88 => { - self.slow_consumer_grace_ms = is.read_uint64()?; - }, - 96 => { - self.exec_sessions_per_vm = is.read_uint32()?; - }, - 104 => { - self.attached_sessions_per_vm = is.read_uint32()?; - }, - 112 => { - self.pending_read_output_waits_per_stream = is.read_uint32()?; - }, - 120 => { - self.pending_exec_waits_per_vm = is.read_uint32()?; - }, - 128 => { - self.rpc_rate_per_connection_per_second = is.read_uint32()?; - }, - 136 => { - self.rpc_rate_per_vm_burst = is.read_uint32()?; - }, - 144 => { - self.shell_sessions_per_vm = is.read_uint32()?; - }, - 152 => { - self.shell_attached_sessions_per_vm = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.max_chunk_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(1, self.max_chunk_bytes); - } - if self.max_recv_message_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.max_recv_message_bytes); - } - if self.decoded_write_stdin_bytes_per_connection != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.decoded_write_stdin_bytes_per_connection); - } - if self.write_stdin_handlers_per_connection != 0 { - my_size += ::protobuf::rt::uint32_size(4, self.write_stdin_handlers_per_connection); - } - if self.stdin_queue_chunks_per_exec != 0 { - my_size += ::protobuf::rt::uint32_size(5, self.stdin_queue_chunks_per_exec); - } - if self.stdout_live_buffer_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(6, self.stdout_live_buffer_bytes); - } - if self.stderr_live_buffer_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(7, self.stderr_live_buffer_bytes); - } - if self.detached_stdout_log_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(8, self.detached_stdout_log_bytes); - } - if self.detached_stderr_log_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(9, self.detached_stderr_log_bytes); - } - if self.long_poll_timeout_ms != 0 { - my_size += ::protobuf::rt::uint64_size(10, self.long_poll_timeout_ms); - } - if self.slow_consumer_grace_ms != 0 { - my_size += ::protobuf::rt::uint64_size(11, self.slow_consumer_grace_ms); - } - if self.exec_sessions_per_vm != 0 { - my_size += ::protobuf::rt::uint32_size(12, self.exec_sessions_per_vm); - } - if self.attached_sessions_per_vm != 0 { - my_size += ::protobuf::rt::uint32_size(13, self.attached_sessions_per_vm); - } - if self.pending_read_output_waits_per_stream != 0 { - my_size += ::protobuf::rt::uint32_size(14, self.pending_read_output_waits_per_stream); - } - if self.pending_exec_waits_per_vm != 0 { - my_size += ::protobuf::rt::uint32_size(15, self.pending_exec_waits_per_vm); - } - if self.rpc_rate_per_connection_per_second != 0 { - my_size += ::protobuf::rt::uint32_size(16, self.rpc_rate_per_connection_per_second); - } - if self.rpc_rate_per_vm_burst != 0 { - my_size += ::protobuf::rt::uint32_size(17, self.rpc_rate_per_vm_burst); - } - if self.shell_sessions_per_vm != 0 { - my_size += ::protobuf::rt::uint32_size(18, self.shell_sessions_per_vm); - } - if self.shell_attached_sessions_per_vm != 0 { - my_size += ::protobuf::rt::uint32_size(19, self.shell_attached_sessions_per_vm); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.max_chunk_bytes != 0 { - os.write_uint64(1, self.max_chunk_bytes)?; - } - if self.max_recv_message_bytes != 0 { - os.write_uint64(2, self.max_recv_message_bytes)?; - } - if self.decoded_write_stdin_bytes_per_connection != 0 { - os.write_uint64(3, self.decoded_write_stdin_bytes_per_connection)?; - } - if self.write_stdin_handlers_per_connection != 0 { - os.write_uint32(4, self.write_stdin_handlers_per_connection)?; - } - if self.stdin_queue_chunks_per_exec != 0 { - os.write_uint32(5, self.stdin_queue_chunks_per_exec)?; - } - if self.stdout_live_buffer_bytes != 0 { - os.write_uint64(6, self.stdout_live_buffer_bytes)?; - } - if self.stderr_live_buffer_bytes != 0 { - os.write_uint64(7, self.stderr_live_buffer_bytes)?; - } - if self.detached_stdout_log_bytes != 0 { - os.write_uint64(8, self.detached_stdout_log_bytes)?; - } - if self.detached_stderr_log_bytes != 0 { - os.write_uint64(9, self.detached_stderr_log_bytes)?; - } - if self.long_poll_timeout_ms != 0 { - os.write_uint64(10, self.long_poll_timeout_ms)?; - } - if self.slow_consumer_grace_ms != 0 { - os.write_uint64(11, self.slow_consumer_grace_ms)?; - } - if self.exec_sessions_per_vm != 0 { - os.write_uint32(12, self.exec_sessions_per_vm)?; - } - if self.attached_sessions_per_vm != 0 { - os.write_uint32(13, self.attached_sessions_per_vm)?; - } - if self.pending_read_output_waits_per_stream != 0 { - os.write_uint32(14, self.pending_read_output_waits_per_stream)?; - } - if self.pending_exec_waits_per_vm != 0 { - os.write_uint32(15, self.pending_exec_waits_per_vm)?; - } - if self.rpc_rate_per_connection_per_second != 0 { - os.write_uint32(16, self.rpc_rate_per_connection_per_second)?; - } - if self.rpc_rate_per_vm_burst != 0 { - os.write_uint32(17, self.rpc_rate_per_vm_burst)?; - } - if self.shell_sessions_per_vm != 0 { - os.write_uint32(18, self.shell_sessions_per_vm)?; - } - if self.shell_attached_sessions_per_vm != 0 { - os.write_uint32(19, self.shell_attached_sessions_per_vm)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> GuestEffectiveLimits { - GuestEffectiveLimits::new() - } - - fn clear(&mut self) { - self.max_chunk_bytes = 0; - self.max_recv_message_bytes = 0; - self.decoded_write_stdin_bytes_per_connection = 0; - self.write_stdin_handlers_per_connection = 0; - self.stdin_queue_chunks_per_exec = 0; - self.stdout_live_buffer_bytes = 0; - self.stderr_live_buffer_bytes = 0; - self.detached_stdout_log_bytes = 0; - self.detached_stderr_log_bytes = 0; - self.long_poll_timeout_ms = 0; - self.slow_consumer_grace_ms = 0; - self.exec_sessions_per_vm = 0; - self.attached_sessions_per_vm = 0; - self.pending_read_output_waits_per_stream = 0; - self.pending_exec_waits_per_vm = 0; - self.rpc_rate_per_connection_per_second = 0; - self.rpc_rate_per_vm_burst = 0; - self.shell_sessions_per_vm = 0; - self.shell_attached_sessions_per_vm = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static GuestEffectiveLimits { - static instance: GuestEffectiveLimits = GuestEffectiveLimits { - max_chunk_bytes: 0, - max_recv_message_bytes: 0, - decoded_write_stdin_bytes_per_connection: 0, - write_stdin_handlers_per_connection: 0, - stdin_queue_chunks_per_exec: 0, - stdout_live_buffer_bytes: 0, - stderr_live_buffer_bytes: 0, - detached_stdout_log_bytes: 0, - detached_stderr_log_bytes: 0, - long_poll_timeout_ms: 0, - slow_consumer_grace_ms: 0, - exec_sessions_per_vm: 0, - attached_sessions_per_vm: 0, - pending_read_output_waits_per_stream: 0, - pending_exec_waits_per_vm: 0, - rpc_rate_per_connection_per_second: 0, - rpc_rate_per_vm_burst: 0, - shell_sessions_per_vm: 0, - shell_attached_sessions_per_vm: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for GuestEffectiveLimits { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("GuestEffectiveLimits").unwrap()).clone() - } -} - -impl ::std::fmt::Display for GuestEffectiveLimits { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for GuestEffectiveLimits { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecCreateRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecCreateRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.argv) - pub argv: ::std::vec::Vec<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.user) - pub user: ::std::option::Option<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.cwd) - pub cwd: ::std::option::Option<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.env) - pub env: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.tty) - pub tty: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.stdin_open) - pub stdin_open: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.detached) - pub detached: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.initial_terminal_size) - pub initial_terminal_size: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateRequest.output_policy) - pub output_policy: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecCreateRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecCreateRequest { - fn default() -> &'a ExecCreateRequest { - ::default_instance() - } -} - -impl ExecCreateRequest { - pub fn new() -> ExecCreateRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(10); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &ExecCreateRequest| { &m.metadata }, - |m: &mut ExecCreateRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "argv", - |m: &ExecCreateRequest| { &m.argv }, - |m: &mut ExecCreateRequest| { &mut m.argv }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "user", - |m: &ExecCreateRequest| { &m.user }, - |m: &mut ExecCreateRequest| { &mut m.user }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "cwd", - |m: &ExecCreateRequest| { &m.cwd }, - |m: &mut ExecCreateRequest| { &mut m.cwd }, - )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "env", - |m: &ExecCreateRequest| { &m.env }, - |m: &mut ExecCreateRequest| { &mut m.env }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "tty", - |m: &ExecCreateRequest| { &m.tty }, - |m: &mut ExecCreateRequest| { &mut m.tty }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdin_open", - |m: &ExecCreateRequest| { &m.stdin_open }, - |m: &mut ExecCreateRequest| { &mut m.stdin_open }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "detached", - |m: &ExecCreateRequest| { &m.detached }, - |m: &mut ExecCreateRequest| { &mut m.detached }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalSize>( - "initial_terminal_size", - |m: &ExecCreateRequest| { &m.initial_terminal_size }, - |m: &mut ExecCreateRequest| { &mut m.initial_terminal_size }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, OutputPolicy>( - "output_policy", - |m: &ExecCreateRequest| { &m.output_policy }, - |m: &mut ExecCreateRequest| { &mut m.output_policy }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecCreateRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecCreateRequest { - const NAME: &'static str = "ExecCreateRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.argv.push(is.read_string()?); - }, - 26 => { - self.user = ::std::option::Option::Some(is.read_string()?); - }, - 34 => { - self.cwd = ::std::option::Option::Some(is.read_string()?); - }, - 42 => { - self.env.push(is.read_message()?); - }, - 48 => { - self.tty = is.read_bool()?; - }, - 56 => { - self.stdin_open = is.read_bool()?; - }, - 64 => { - self.detached = is.read_bool()?; - }, - 74 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.initial_terminal_size)?; - }, - 82 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.output_policy)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - for value in &self.argv { - my_size += ::protobuf::rt::string_size(2, &value); - }; - if let Some(v) = self.user.as_ref() { - my_size += ::protobuf::rt::string_size(3, &v); - } - if let Some(v) = self.cwd.as_ref() { - my_size += ::protobuf::rt::string_size(4, &v); - } - for value in &self.env { - let len = value.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - }; - if self.tty != false { - my_size += 1 + 1; - } - if self.stdin_open != false { - my_size += 1 + 1; - } - if self.detached != false { - my_size += 1 + 1; - } - if let Some(v) = self.initial_terminal_size.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.output_policy.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - for v in &self.argv { - os.write_string(2, &v)?; - }; - if let Some(v) = self.user.as_ref() { - os.write_string(3, v)?; - } - if let Some(v) = self.cwd.as_ref() { - os.write_string(4, v)?; - } - for v in &self.env { - ::protobuf::rt::write_message_field_with_cached_size(5, v, os)?; - }; - if self.tty != false { - os.write_bool(6, self.tty)?; - } - if self.stdin_open != false { - os.write_bool(7, self.stdin_open)?; - } - if self.detached != false { - os.write_bool(8, self.detached)?; - } - if let Some(v) = self.initial_terminal_size.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(9, v, os)?; - } - if let Some(v) = self.output_policy.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(10, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecCreateRequest { - ExecCreateRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.argv.clear(); - self.user = ::std::option::Option::None; - self.cwd = ::std::option::Option::None; - self.env.clear(); - self.tty = false; - self.stdin_open = false; - self.detached = false; - self.initial_terminal_size.clear(); - self.output_policy.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecCreateRequest { - static instance: ExecCreateRequest = ExecCreateRequest { - metadata: ::protobuf::MessageField::none(), - argv: ::std::vec::Vec::new(), - user: ::std::option::Option::None, - cwd: ::std::option::Option::None, - env: ::std::vec::Vec::new(), - tty: false, - stdin_open: false, - detached: false, - initial_terminal_size: ::protobuf::MessageField::none(), - output_policy: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecCreateRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecCreateRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecCreateRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecCreateRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.EnvVar) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct EnvVar { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.EnvVar.key) - pub key: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.EnvVar.value) - pub value: ::std::string::String, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.EnvVar.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a EnvVar { - fn default() -> &'a EnvVar { - ::default_instance() - } -} - -impl EnvVar { - pub fn new() -> EnvVar { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "key", - |m: &EnvVar| { &m.key }, - |m: &mut EnvVar| { &mut m.key }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "value", - |m: &EnvVar| { &m.value }, - |m: &mut EnvVar| { &mut m.value }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "EnvVar", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for EnvVar { - const NAME: &'static str = "EnvVar"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.key = is.read_string()?; - }, - 18 => { - self.value = is.read_string()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.key.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.key); - } - if !self.value.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.value); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.key.is_empty() { - os.write_string(1, &self.key)?; - } - if !self.value.is_empty() { - os.write_string(2, &self.value)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> EnvVar { - EnvVar::new() - } - - fn clear(&mut self) { - self.key.clear(); - self.value.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static EnvVar { - static instance: EnvVar = EnvVar { - key: ::std::string::String::new(), - value: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for EnvVar { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("EnvVar").unwrap()).clone() - } -} - -impl ::std::fmt::Display for EnvVar { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for EnvVar { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TerminalSize) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TerminalSize { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalSize.rows) - pub rows: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalSize.cols) - pub cols: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TerminalSize.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TerminalSize { - fn default() -> &'a TerminalSize { - ::default_instance() - } -} - -impl TerminalSize { - pub fn new() -> TerminalSize { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "rows", - |m: &TerminalSize| { &m.rows }, - |m: &mut TerminalSize| { &mut m.rows }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "cols", - |m: &TerminalSize| { &m.cols }, - |m: &mut TerminalSize| { &mut m.cols }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TerminalSize", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TerminalSize { - const NAME: &'static str = "TerminalSize"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.rows = is.read_uint32()?; - }, - 16 => { - self.cols = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.rows != 0 { - my_size += ::protobuf::rt::uint32_size(1, self.rows); - } - if self.cols != 0 { - my_size += ::protobuf::rt::uint32_size(2, self.cols); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.rows != 0 { - os.write_uint32(1, self.rows)?; - } - if self.cols != 0 { - os.write_uint32(2, self.cols)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TerminalSize { - TerminalSize::new() - } - - fn clear(&mut self) { - self.rows = 0; - self.cols = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static TerminalSize { - static instance: TerminalSize = TerminalSize { - rows: 0, - cols: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TerminalSize { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TerminalSize").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TerminalSize { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TerminalSize { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.OutputPolicy) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct OutputPolicy { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.OutputPolicy.max_chunk_bytes) - pub max_chunk_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.OutputPolicy.max_stdout_log_bytes) - pub max_stdout_log_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.OutputPolicy.max_stderr_log_bytes) - pub max_stderr_log_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.OutputPolicy.slow_consumer_timeout_ms) - pub slow_consumer_timeout_ms: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.OutputPolicy.wait_timeout_ms) - pub wait_timeout_ms: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.OutputPolicy.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a OutputPolicy { - fn default() -> &'a OutputPolicy { - ::default_instance() - } -} - -impl OutputPolicy { - pub fn new() -> OutputPolicy { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(5); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_chunk_bytes", - |m: &OutputPolicy| { &m.max_chunk_bytes }, - |m: &mut OutputPolicy| { &mut m.max_chunk_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_stdout_log_bytes", - |m: &OutputPolicy| { &m.max_stdout_log_bytes }, - |m: &mut OutputPolicy| { &mut m.max_stdout_log_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_stderr_log_bytes", - |m: &OutputPolicy| { &m.max_stderr_log_bytes }, - |m: &mut OutputPolicy| { &mut m.max_stderr_log_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "slow_consumer_timeout_ms", - |m: &OutputPolicy| { &m.slow_consumer_timeout_ms }, - |m: &mut OutputPolicy| { &mut m.slow_consumer_timeout_ms }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "wait_timeout_ms", - |m: &OutputPolicy| { &m.wait_timeout_ms }, - |m: &mut OutputPolicy| { &mut m.wait_timeout_ms }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "OutputPolicy", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for OutputPolicy { - const NAME: &'static str = "OutputPolicy"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.max_chunk_bytes = is.read_uint64()?; - }, - 16 => { - self.max_stdout_log_bytes = is.read_uint64()?; - }, - 24 => { - self.max_stderr_log_bytes = is.read_uint64()?; - }, - 32 => { - self.slow_consumer_timeout_ms = is.read_uint64()?; - }, - 40 => { - self.wait_timeout_ms = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.max_chunk_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(1, self.max_chunk_bytes); - } - if self.max_stdout_log_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.max_stdout_log_bytes); - } - if self.max_stderr_log_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.max_stderr_log_bytes); - } - if self.slow_consumer_timeout_ms != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.slow_consumer_timeout_ms); - } - if self.wait_timeout_ms != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.wait_timeout_ms); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.max_chunk_bytes != 0 { - os.write_uint64(1, self.max_chunk_bytes)?; - } - if self.max_stdout_log_bytes != 0 { - os.write_uint64(2, self.max_stdout_log_bytes)?; - } - if self.max_stderr_log_bytes != 0 { - os.write_uint64(3, self.max_stderr_log_bytes)?; - } - if self.slow_consumer_timeout_ms != 0 { - os.write_uint64(4, self.slow_consumer_timeout_ms)?; - } - if self.wait_timeout_ms != 0 { - os.write_uint64(5, self.wait_timeout_ms)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> OutputPolicy { - OutputPolicy::new() - } - - fn clear(&mut self) { - self.max_chunk_bytes = 0; - self.max_stdout_log_bytes = 0; - self.max_stderr_log_bytes = 0; - self.slow_consumer_timeout_ms = 0; - self.wait_timeout_ms = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static OutputPolicy { - static instance: OutputPolicy = OutputPolicy { - max_chunk_bytes: 0, - max_stdout_log_bytes: 0, - max_stderr_log_bytes: 0, - slow_consumer_timeout_ms: 0, - wait_timeout_ms: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for OutputPolicy { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("OutputPolicy").unwrap()).clone() - } -} - -impl ::std::fmt::Display for OutputPolicy { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for OutputPolicy { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecCreateResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecCreateResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.exec_id) - pub exec_id: ::std::option::Option<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.created_at_monotonic_ns) - pub created_at_monotonic_ns: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.control_seq) - pub control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.stdout_cursor) - pub stdout_cursor: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.stderr_cursor) - pub stderr_cursor: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.effective_limits) - pub effective_limits: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCreateResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecCreateResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecCreateResponse { - fn default() -> &'a ExecCreateResponse { - ::default_instance() - } -} - -impl ExecCreateResponse { - pub fn new() -> ExecCreateResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(8); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "exec_id", - |m: &ExecCreateResponse| { &m.exec_id }, - |m: &mut ExecCreateResponse| { &mut m.exec_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "created_at_monotonic_ns", - |m: &ExecCreateResponse| { &m.created_at_monotonic_ns }, - |m: &mut ExecCreateResponse| { &mut m.created_at_monotonic_ns }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "control_seq", - |m: &ExecCreateResponse| { &m.control_seq }, - |m: &mut ExecCreateResponse| { &mut m.control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_cursor", - |m: &ExecCreateResponse| { &m.stdout_cursor }, - |m: &mut ExecCreateResponse| { &mut m.stdout_cursor }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_cursor", - |m: &ExecCreateResponse| { &m.stderr_cursor }, - |m: &mut ExecCreateResponse| { &mut m.stderr_cursor }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestEffectiveLimits>( - "effective_limits", - |m: &ExecCreateResponse| { &m.effective_limits }, - |m: &mut ExecCreateResponse| { &mut m.effective_limits }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &ExecCreateResponse| { &m.state }, - |m: &mut ExecCreateResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ExecCreateResponse| { &m.error }, - |m: &mut ExecCreateResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecCreateResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecCreateResponse { - const NAME: &'static str = "ExecCreateResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.exec_id = ::std::option::Option::Some(is.read_string()?); - }, - 16 => { - self.created_at_monotonic_ns = is.read_uint64()?; - }, - 24 => { - self.control_seq = is.read_uint64()?; - }, - 32 => { - self.stdout_cursor = is.read_uint64()?; - }, - 40 => { - self.stderr_cursor = is.read_uint64()?; - }, - 50 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.effective_limits)?; - }, - 56 => { - self.state = is.read_enum_or_unknown()?; - }, - 66 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.exec_id.as_ref() { - my_size += ::protobuf::rt::string_size(1, &v); - } - if self.created_at_monotonic_ns != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.created_at_monotonic_ns); - } - if self.control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.control_seq); - } - if self.stdout_cursor != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.stdout_cursor); - } - if self.stderr_cursor != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.stderr_cursor); - } - if let Some(v) = self.effective_limits.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(7, self.state.value()); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.exec_id.as_ref() { - os.write_string(1, v)?; - } - if self.created_at_monotonic_ns != 0 { - os.write_uint64(2, self.created_at_monotonic_ns)?; - } - if self.control_seq != 0 { - os.write_uint64(3, self.control_seq)?; - } - if self.stdout_cursor != 0 { - os.write_uint64(4, self.stdout_cursor)?; - } - if self.stderr_cursor != 0 { - os.write_uint64(5, self.stderr_cursor)?; - } - if let Some(v) = self.effective_limits.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(6, v, os)?; - } - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - os.write_enum(7, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(8, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecCreateResponse { - ExecCreateResponse::new() - } - - fn clear(&mut self) { - self.exec_id = ::std::option::Option::None; - self.created_at_monotonic_ns = 0; - self.control_seq = 0; - self.stdout_cursor = 0; - self.stderr_cursor = 0; - self.effective_limits.clear(); - self.state = ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecCreateResponse { - static instance: ExecCreateResponse = ExecCreateResponse { - exec_id: ::std::option::Option::None, - created_at_monotonic_ns: 0, - control_seq: 0, - stdout_cursor: 0, - stderr_cursor: 0, - effective_limits: ::protobuf::MessageField::none(), - state: ::protobuf::EnumOrUnknown::from_i32(0), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecCreateResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecCreateResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecCreateResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecCreateResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecInspectRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecInspectRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectRequest.metadata) - pub metadata: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecInspectRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecInspectRequest { - fn default() -> &'a ExecInspectRequest { - ::default_instance() - } -} - -impl ExecInspectRequest { - pub fn new() -> ExecInspectRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(1); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &ExecInspectRequest| { &m.metadata }, - |m: &mut ExecInspectRequest| { &mut m.metadata }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecInspectRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecInspectRequest { - const NAME: &'static str = "ExecInspectRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecInspectRequest { - ExecInspectRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecInspectRequest { - static instance: ExecInspectRequest = ExecInspectRequest { - metadata: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecInspectRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecInspectRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecInspectRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecInspectRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecInspectResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecInspectResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.visible_terminal_status) - pub visible_terminal_status: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stdin_state) - pub stdin_state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stdout_start_offset) - pub stdout_start_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stdout_end_offset) - pub stdout_end_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stderr_start_offset) - pub stderr_start_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stderr_end_offset) - pub stderr_end_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stdout_dropped_bytes) - pub stdout_dropped_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stderr_dropped_bytes) - pub stderr_dropped_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stdout_truncated_for_retention) - pub stdout_truncated_for_retention: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.stderr_truncated_for_retention) - pub stderr_truncated_for_retention: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.last_control_seq) - pub last_control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.state_generation) - pub state_generation: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecInspectResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecInspectResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecInspectResponse { - fn default() -> &'a ExecInspectResponse { - ::default_instance() - } -} - -impl ExecInspectResponse { - pub fn new() -> ExecInspectResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(14); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &ExecInspectResponse| { &m.state }, - |m: &mut ExecInspectResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalStatus>( - "visible_terminal_status", - |m: &ExecInspectResponse| { &m.visible_terminal_status }, - |m: &mut ExecInspectResponse| { &mut m.visible_terminal_status }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdin_state", - |m: &ExecInspectResponse| { &m.stdin_state }, - |m: &mut ExecInspectResponse| { &mut m.stdin_state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_start_offset", - |m: &ExecInspectResponse| { &m.stdout_start_offset }, - |m: &mut ExecInspectResponse| { &mut m.stdout_start_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_end_offset", - |m: &ExecInspectResponse| { &m.stdout_end_offset }, - |m: &mut ExecInspectResponse| { &mut m.stdout_end_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_start_offset", - |m: &ExecInspectResponse| { &m.stderr_start_offset }, - |m: &mut ExecInspectResponse| { &mut m.stderr_start_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_end_offset", - |m: &ExecInspectResponse| { &m.stderr_end_offset }, - |m: &mut ExecInspectResponse| { &mut m.stderr_end_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_dropped_bytes", - |m: &ExecInspectResponse| { &m.stdout_dropped_bytes }, - |m: &mut ExecInspectResponse| { &mut m.stdout_dropped_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_dropped_bytes", - |m: &ExecInspectResponse| { &m.stderr_dropped_bytes }, - |m: &mut ExecInspectResponse| { &mut m.stderr_dropped_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_truncated_for_retention", - |m: &ExecInspectResponse| { &m.stdout_truncated_for_retention }, - |m: &mut ExecInspectResponse| { &mut m.stdout_truncated_for_retention }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_truncated_for_retention", - |m: &ExecInspectResponse| { &m.stderr_truncated_for_retention }, - |m: &mut ExecInspectResponse| { &mut m.stderr_truncated_for_retention }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "last_control_seq", - |m: &ExecInspectResponse| { &m.last_control_seq }, - |m: &mut ExecInspectResponse| { &mut m.last_control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state_generation", - |m: &ExecInspectResponse| { &m.state_generation }, - |m: &mut ExecInspectResponse| { &mut m.state_generation }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ExecInspectResponse| { &m.error }, - |m: &mut ExecInspectResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecInspectResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecInspectResponse { - const NAME: &'static str = "ExecInspectResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.state = is.read_enum_or_unknown()?; - }, - 18 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.visible_terminal_status)?; - }, - 24 => { - self.stdin_state = is.read_enum_or_unknown()?; - }, - 32 => { - self.stdout_start_offset = is.read_uint64()?; - }, - 40 => { - self.stdout_end_offset = is.read_uint64()?; - }, - 48 => { - self.stderr_start_offset = is.read_uint64()?; - }, - 56 => { - self.stderr_end_offset = is.read_uint64()?; - }, - 64 => { - self.stdout_dropped_bytes = is.read_uint64()?; - }, - 72 => { - self.stderr_dropped_bytes = is.read_uint64()?; - }, - 80 => { - self.stdout_truncated_for_retention = is.read_bool()?; - }, - 88 => { - self.stderr_truncated_for_retention = is.read_bool()?; - }, - 96 => { - self.last_control_seq = is.read_uint64()?; - }, - 104 => { - self.state_generation = is.read_uint64()?; - }, - 114 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.state.value()); - } - if let Some(v) = self.visible_terminal_status.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.stdin_state != ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.stdin_state.value()); - } - if self.stdout_start_offset != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.stdout_start_offset); - } - if self.stdout_end_offset != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.stdout_end_offset); - } - if self.stderr_start_offset != 0 { - my_size += ::protobuf::rt::uint64_size(6, self.stderr_start_offset); - } - if self.stderr_end_offset != 0 { - my_size += ::protobuf::rt::uint64_size(7, self.stderr_end_offset); - } - if self.stdout_dropped_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(8, self.stdout_dropped_bytes); - } - if self.stderr_dropped_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(9, self.stderr_dropped_bytes); - } - if self.stdout_truncated_for_retention != false { - my_size += 1 + 1; - } - if self.stderr_truncated_for_retention != false { - my_size += 1 + 1; - } - if self.last_control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(12, self.last_control_seq); - } - if self.state_generation != 0 { - my_size += ::protobuf::rt::uint64_size(13, self.state_generation); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if let Some(v) = self.visible_terminal_status.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; - } - if self.stdin_state != ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.stdin_state))?; - } - if self.stdout_start_offset != 0 { - os.write_uint64(4, self.stdout_start_offset)?; - } - if self.stdout_end_offset != 0 { - os.write_uint64(5, self.stdout_end_offset)?; - } - if self.stderr_start_offset != 0 { - os.write_uint64(6, self.stderr_start_offset)?; - } - if self.stderr_end_offset != 0 { - os.write_uint64(7, self.stderr_end_offset)?; - } - if self.stdout_dropped_bytes != 0 { - os.write_uint64(8, self.stdout_dropped_bytes)?; - } - if self.stderr_dropped_bytes != 0 { - os.write_uint64(9, self.stderr_dropped_bytes)?; - } - if self.stdout_truncated_for_retention != false { - os.write_bool(10, self.stdout_truncated_for_retention)?; - } - if self.stderr_truncated_for_retention != false { - os.write_bool(11, self.stderr_truncated_for_retention)?; - } - if self.last_control_seq != 0 { - os.write_uint64(12, self.last_control_seq)?; - } - if self.state_generation != 0 { - os.write_uint64(13, self.state_generation)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(14, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecInspectResponse { - ExecInspectResponse::new() - } - - fn clear(&mut self) { - self.state = ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED); - self.visible_terminal_status.clear(); - self.stdin_state = ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED); - self.stdout_start_offset = 0; - self.stdout_end_offset = 0; - self.stderr_start_offset = 0; - self.stderr_end_offset = 0; - self.stdout_dropped_bytes = 0; - self.stderr_dropped_bytes = 0; - self.stdout_truncated_for_retention = false; - self.stderr_truncated_for_retention = false; - self.last_control_seq = 0; - self.state_generation = 0; - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecInspectResponse { - static instance: ExecInspectResponse = ExecInspectResponse { - state: ::protobuf::EnumOrUnknown::from_i32(0), - visible_terminal_status: ::protobuf::MessageField::none(), - stdin_state: ::protobuf::EnumOrUnknown::from_i32(0), - stdout_start_offset: 0, - stdout_end_offset: 0, - stderr_start_offset: 0, - stderr_end_offset: 0, - stdout_dropped_bytes: 0, - stderr_dropped_bytes: 0, - stdout_truncated_for_retention: false, - stderr_truncated_for_retention: false, - last_control_seq: 0, - state_generation: 0, - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecInspectResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecInspectResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecInspectResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecInspectResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecWaitRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecWaitRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitRequest.timeout_ms) - pub timeout_ms: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitRequest.known_state_generation) - pub known_state_generation: ::std::option::Option, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecWaitRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecWaitRequest { - fn default() -> &'a ExecWaitRequest { - ::default_instance() - } -} - -impl ExecWaitRequest { - pub fn new() -> ExecWaitRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &ExecWaitRequest| { &m.metadata }, - |m: &mut ExecWaitRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "timeout_ms", - |m: &ExecWaitRequest| { &m.timeout_ms }, - |m: &mut ExecWaitRequest| { &mut m.timeout_ms }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "known_state_generation", - |m: &ExecWaitRequest| { &m.known_state_generation }, - |m: &mut ExecWaitRequest| { &mut m.known_state_generation }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecWaitRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecWaitRequest { - const NAME: &'static str = "ExecWaitRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.timeout_ms = is.read_uint64()?; - }, - 24 => { - self.known_state_generation = ::std::option::Option::Some(is.read_uint64()?); - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.timeout_ms != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.timeout_ms); - } - if let Some(v) = self.known_state_generation { - my_size += ::protobuf::rt::uint64_size(3, v); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.timeout_ms != 0 { - os.write_uint64(2, self.timeout_ms)?; - } - if let Some(v) = self.known_state_generation { - os.write_uint64(3, v)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecWaitRequest { - ExecWaitRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.timeout_ms = 0; - self.known_state_generation = ::std::option::Option::None; - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecWaitRequest { - static instance: ExecWaitRequest = ExecWaitRequest { - metadata: ::protobuf::MessageField::none(), - timeout_ms: 0, - known_state_generation: ::std::option::Option::None, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecWaitRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecWaitRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecWaitRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecWaitRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecWaitResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecWaitResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.visible_terminal_status) - pub visible_terminal_status: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.state_generation) - pub state_generation: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.stdout_start_offset) - pub stdout_start_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.stdout_end_offset) - pub stdout_end_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.stderr_start_offset) - pub stderr_start_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.stderr_end_offset) - pub stderr_end_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.timed_out) - pub timed_out: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecWaitResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecWaitResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecWaitResponse { - fn default() -> &'a ExecWaitResponse { - ::default_instance() - } -} - -impl ExecWaitResponse { - pub fn new() -> ExecWaitResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(9); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &ExecWaitResponse| { &m.state }, - |m: &mut ExecWaitResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalStatus>( - "visible_terminal_status", - |m: &ExecWaitResponse| { &m.visible_terminal_status }, - |m: &mut ExecWaitResponse| { &mut m.visible_terminal_status }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state_generation", - |m: &ExecWaitResponse| { &m.state_generation }, - |m: &mut ExecWaitResponse| { &mut m.state_generation }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_start_offset", - |m: &ExecWaitResponse| { &m.stdout_start_offset }, - |m: &mut ExecWaitResponse| { &mut m.stdout_start_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_end_offset", - |m: &ExecWaitResponse| { &m.stdout_end_offset }, - |m: &mut ExecWaitResponse| { &mut m.stdout_end_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_start_offset", - |m: &ExecWaitResponse| { &m.stderr_start_offset }, - |m: &mut ExecWaitResponse| { &mut m.stderr_start_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_end_offset", - |m: &ExecWaitResponse| { &m.stderr_end_offset }, - |m: &mut ExecWaitResponse| { &mut m.stderr_end_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "timed_out", - |m: &ExecWaitResponse| { &m.timed_out }, - |m: &mut ExecWaitResponse| { &mut m.timed_out }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ExecWaitResponse| { &m.error }, - |m: &mut ExecWaitResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecWaitResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecWaitResponse { - const NAME: &'static str = "ExecWaitResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.state = is.read_enum_or_unknown()?; - }, - 18 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.visible_terminal_status)?; - }, - 24 => { - self.state_generation = is.read_uint64()?; - }, - 32 => { - self.stdout_start_offset = is.read_uint64()?; - }, - 40 => { - self.stdout_end_offset = is.read_uint64()?; - }, - 48 => { - self.stderr_start_offset = is.read_uint64()?; - }, - 56 => { - self.stderr_end_offset = is.read_uint64()?; - }, - 64 => { - self.timed_out = is.read_bool()?; - }, - 74 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.state.value()); - } - if let Some(v) = self.visible_terminal_status.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.state_generation != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.state_generation); - } - if self.stdout_start_offset != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.stdout_start_offset); - } - if self.stdout_end_offset != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.stdout_end_offset); - } - if self.stderr_start_offset != 0 { - my_size += ::protobuf::rt::uint64_size(6, self.stderr_start_offset); - } - if self.stderr_end_offset != 0 { - my_size += ::protobuf::rt::uint64_size(7, self.stderr_end_offset); - } - if self.timed_out != false { - my_size += 1 + 1; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if let Some(v) = self.visible_terminal_status.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; - } - if self.state_generation != 0 { - os.write_uint64(3, self.state_generation)?; - } - if self.stdout_start_offset != 0 { - os.write_uint64(4, self.stdout_start_offset)?; - } - if self.stdout_end_offset != 0 { - os.write_uint64(5, self.stdout_end_offset)?; - } - if self.stderr_start_offset != 0 { - os.write_uint64(6, self.stderr_start_offset)?; - } - if self.stderr_end_offset != 0 { - os.write_uint64(7, self.stderr_end_offset)?; - } - if self.timed_out != false { - os.write_bool(8, self.timed_out)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(9, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecWaitResponse { - ExecWaitResponse::new() - } - - fn clear(&mut self) { - self.state = ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED); - self.visible_terminal_status.clear(); - self.state_generation = 0; - self.stdout_start_offset = 0; - self.stdout_end_offset = 0; - self.stderr_start_offset = 0; - self.stderr_end_offset = 0; - self.timed_out = false; - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecWaitResponse { - static instance: ExecWaitResponse = ExecWaitResponse { - state: ::protobuf::EnumOrUnknown::from_i32(0), - visible_terminal_status: ::protobuf::MessageField::none(), - state_generation: 0, - stdout_start_offset: 0, - stdout_end_offset: 0, - stderr_start_offset: 0, - stderr_end_offset: 0, - timed_out: false, - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecWaitResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecWaitResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecWaitResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecWaitResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecLogsRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecLogsRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsRequest.stream) - pub stream: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsRequest.offset) - pub offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsRequest.max_len) - pub max_len: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecLogsRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecLogsRequest { - fn default() -> &'a ExecLogsRequest { - ::default_instance() - } -} - -impl ExecLogsRequest { - pub fn new() -> ExecLogsRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &ExecLogsRequest| { &m.metadata }, - |m: &mut ExecLogsRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stream", - |m: &ExecLogsRequest| { &m.stream }, - |m: &mut ExecLogsRequest| { &mut m.stream }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &ExecLogsRequest| { &m.offset }, - |m: &mut ExecLogsRequest| { &mut m.offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_len", - |m: &ExecLogsRequest| { &m.max_len }, - |m: &mut ExecLogsRequest| { &mut m.max_len }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecLogsRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecLogsRequest { - const NAME: &'static str = "ExecLogsRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.stream = is.read_enum_or_unknown()?; - }, - 24 => { - self.offset = is.read_uint64()?; - }, - 32 => { - self.max_len = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.stream.value()); - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.offset); - } - if self.max_len != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.max_len); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.stream))?; - } - if self.offset != 0 { - os.write_uint64(3, self.offset)?; - } - if self.max_len != 0 { - os.write_uint64(4, self.max_len)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecLogsRequest { - ExecLogsRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.stream = ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED); - self.offset = 0; - self.max_len = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecLogsRequest { - static instance: ExecLogsRequest = ExecLogsRequest { - metadata: ::protobuf::MessageField::none(), - stream: ::protobuf::EnumOrUnknown::from_i32(0), - offset: 0, - max_len: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecLogsRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecLogsRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecLogsRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecLogsRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecLogsResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecLogsResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.stream) - pub stream: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.offset) - pub offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.end_offset) - pub end_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.data) - pub data: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.next_offset) - pub next_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.eof) - pub eof: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.start_offset) - pub start_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.dropped_bytes) - pub dropped_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.truncated) - pub truncated: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecLogsResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecLogsResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecLogsResponse { - fn default() -> &'a ExecLogsResponse { - ::default_instance() - } -} - -impl ExecLogsResponse { - pub fn new() -> ExecLogsResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(10); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stream", - |m: &ExecLogsResponse| { &m.stream }, - |m: &mut ExecLogsResponse| { &mut m.stream }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &ExecLogsResponse| { &m.offset }, - |m: &mut ExecLogsResponse| { &mut m.offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "end_offset", - |m: &ExecLogsResponse| { &m.end_offset }, - |m: &mut ExecLogsResponse| { &mut m.end_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "data", - |m: &ExecLogsResponse| { &m.data }, - |m: &mut ExecLogsResponse| { &mut m.data }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "next_offset", - |m: &ExecLogsResponse| { &m.next_offset }, - |m: &mut ExecLogsResponse| { &mut m.next_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "eof", - |m: &ExecLogsResponse| { &m.eof }, - |m: &mut ExecLogsResponse| { &mut m.eof }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "start_offset", - |m: &ExecLogsResponse| { &m.start_offset }, - |m: &mut ExecLogsResponse| { &mut m.start_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "dropped_bytes", - |m: &ExecLogsResponse| { &m.dropped_bytes }, - |m: &mut ExecLogsResponse| { &mut m.dropped_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "truncated", - |m: &ExecLogsResponse| { &m.truncated }, - |m: &mut ExecLogsResponse| { &mut m.truncated }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ExecLogsResponse| { &m.error }, - |m: &mut ExecLogsResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecLogsResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecLogsResponse { - const NAME: &'static str = "ExecLogsResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.stream = is.read_enum_or_unknown()?; - }, - 16 => { - self.offset = is.read_uint64()?; - }, - 24 => { - self.end_offset = is.read_uint64()?; - }, - 34 => { - self.data = is.read_bytes()?; - }, - 40 => { - self.next_offset = is.read_uint64()?; - }, - 48 => { - self.eof = is.read_bool()?; - }, - 56 => { - self.start_offset = is.read_uint64()?; - }, - 64 => { - self.dropped_bytes = is.read_uint64()?; - }, - 72 => { - self.truncated = is.read_bool()?; - }, - 82 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.stream.value()); - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.offset); - } - if self.end_offset != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.end_offset); - } - if !self.data.is_empty() { - my_size += ::protobuf::rt::bytes_size(4, &self.data); - } - if self.next_offset != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.next_offset); - } - if self.eof != false { - my_size += 1 + 1; - } - if self.start_offset != 0 { - my_size += ::protobuf::rt::uint64_size(7, self.start_offset); - } - if self.dropped_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(8, self.dropped_bytes); - } - if self.truncated != false { - my_size += 1 + 1; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.stream))?; - } - if self.offset != 0 { - os.write_uint64(2, self.offset)?; - } - if self.end_offset != 0 { - os.write_uint64(3, self.end_offset)?; - } - if !self.data.is_empty() { - os.write_bytes(4, &self.data)?; - } - if self.next_offset != 0 { - os.write_uint64(5, self.next_offset)?; - } - if self.eof != false { - os.write_bool(6, self.eof)?; - } - if self.start_offset != 0 { - os.write_uint64(7, self.start_offset)?; - } - if self.dropped_bytes != 0 { - os.write_uint64(8, self.dropped_bytes)?; - } - if self.truncated != false { - os.write_bool(9, self.truncated)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(10, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecLogsResponse { - ExecLogsResponse::new() - } - - fn clear(&mut self) { - self.stream = ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED); - self.offset = 0; - self.end_offset = 0; - self.data.clear(); - self.next_offset = 0; - self.eof = false; - self.start_offset = 0; - self.dropped_bytes = 0; - self.truncated = false; - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecLogsResponse { - static instance: ExecLogsResponse = ExecLogsResponse { - stream: ::protobuf::EnumOrUnknown::from_i32(0), - offset: 0, - end_offset: 0, - data: ::std::vec::Vec::new(), - next_offset: 0, - eof: false, - start_offset: 0, - dropped_bytes: 0, - truncated: false, - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecLogsResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecLogsResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecLogsResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecLogsResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecListRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecListRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListRequest.guest_boot_id) - pub guest_boot_id: ::std::string::String, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecListRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecListRequest { - fn default() -> &'a ExecListRequest { - ::default_instance() - } -} - -impl ExecListRequest { - pub fn new() -> ExecListRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &ExecListRequest| { &m.metadata }, - |m: &mut ExecListRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "guest_boot_id", - |m: &ExecListRequest| { &m.guest_boot_id }, - |m: &mut ExecListRequest| { &mut m.guest_boot_id }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecListRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecListRequest { - const NAME: &'static str = "ExecListRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.guest_boot_id = is.read_string()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.guest_boot_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.guest_boot_id); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.guest_boot_id.is_empty() { - os.write_string(2, &self.guest_boot_id)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecListRequest { - ExecListRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.guest_boot_id.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecListRequest { - static instance: ExecListRequest = ExecListRequest { - metadata: ::protobuf::MessageField::none(), - guest_boot_id: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecListRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecListRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecListRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecListRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecListResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecListResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListResponse.entries) - pub entries: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecListResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecListResponse { - fn default() -> &'a ExecListResponse { - ::default_instance() - } -} - -impl ExecListResponse { - pub fn new() -> ExecListResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "entries", - |m: &ExecListResponse| { &m.entries }, - |m: &mut ExecListResponse| { &mut m.entries }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ExecListResponse| { &m.error }, - |m: &mut ExecListResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecListResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecListResponse { - const NAME: &'static str = "ExecListResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.entries.push(is.read_message()?); - }, - 18 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - for value in &self.entries { - let len = value.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - }; - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - for v in &self.entries { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - }; - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecListResponse { - ExecListResponse::new() - } - - fn clear(&mut self) { - self.entries.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecListResponse { - static instance: ExecListResponse = ExecListResponse { - entries: ::std::vec::Vec::new(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecListResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecListResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecListResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecListResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecListEntry) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecListEntry { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.exec_id) - pub exec_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.slot) - pub slot: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.create_time_unix) - pub create_time_unix: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.argv_sha256) - pub argv_sha256: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.stdout_truncated) - pub stdout_truncated: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.stderr_truncated) - pub stderr_truncated: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecListEntry.dropped_bytes) - pub dropped_bytes: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecListEntry.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecListEntry { - fn default() -> &'a ExecListEntry { - ::default_instance() - } -} - -impl ExecListEntry { - pub fn new() -> ExecListEntry { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(8); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "exec_id", - |m: &ExecListEntry| { &m.exec_id }, - |m: &mut ExecListEntry| { &mut m.exec_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "slot", - |m: &ExecListEntry| { &m.slot }, - |m: &mut ExecListEntry| { &mut m.slot }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &ExecListEntry| { &m.state }, - |m: &mut ExecListEntry| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "create_time_unix", - |m: &ExecListEntry| { &m.create_time_unix }, - |m: &mut ExecListEntry| { &mut m.create_time_unix }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "argv_sha256", - |m: &ExecListEntry| { &m.argv_sha256 }, - |m: &mut ExecListEntry| { &mut m.argv_sha256 }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdout_truncated", - |m: &ExecListEntry| { &m.stdout_truncated }, - |m: &mut ExecListEntry| { &mut m.stdout_truncated }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stderr_truncated", - |m: &ExecListEntry| { &m.stderr_truncated }, - |m: &mut ExecListEntry| { &mut m.stderr_truncated }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "dropped_bytes", - |m: &ExecListEntry| { &m.dropped_bytes }, - |m: &mut ExecListEntry| { &mut m.dropped_bytes }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecListEntry", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecListEntry { - const NAME: &'static str = "ExecListEntry"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.exec_id = is.read_string()?; - }, - 16 => { - self.slot = is.read_uint32()?; - }, - 24 => { - self.state = is.read_enum_or_unknown()?; - }, - 32 => { - self.create_time_unix = is.read_uint64()?; - }, - 42 => { - self.argv_sha256 = is.read_string()?; - }, - 48 => { - self.stdout_truncated = is.read_bool()?; - }, - 56 => { - self.stderr_truncated = is.read_bool()?; - }, - 64 => { - self.dropped_bytes = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.exec_id.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.exec_id); - } - if self.slot != 0 { - my_size += ::protobuf::rt::uint32_size(2, self.slot); - } - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.state.value()); - } - if self.create_time_unix != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.create_time_unix); - } - if !self.argv_sha256.is_empty() { - my_size += ::protobuf::rt::string_size(5, &self.argv_sha256); - } - if self.stdout_truncated != false { - my_size += 1 + 1; - } - if self.stderr_truncated != false { - my_size += 1 + 1; - } - if self.dropped_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(8, self.dropped_bytes); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.exec_id.is_empty() { - os.write_string(1, &self.exec_id)?; - } - if self.slot != 0 { - os.write_uint32(2, self.slot)?; - } - if self.state != ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if self.create_time_unix != 0 { - os.write_uint64(4, self.create_time_unix)?; - } - if !self.argv_sha256.is_empty() { - os.write_string(5, &self.argv_sha256)?; - } - if self.stdout_truncated != false { - os.write_bool(6, self.stdout_truncated)?; - } - if self.stderr_truncated != false { - os.write_bool(7, self.stderr_truncated)?; - } - if self.dropped_bytes != 0 { - os.write_uint64(8, self.dropped_bytes)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecListEntry { - ExecListEntry::new() - } - - fn clear(&mut self) { - self.exec_id.clear(); - self.slot = 0; - self.state = ::protobuf::EnumOrUnknown::new(ExecState::EXEC_STATE_UNSPECIFIED); - self.create_time_unix = 0; - self.argv_sha256.clear(); - self.stdout_truncated = false; - self.stderr_truncated = false; - self.dropped_bytes = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecListEntry { - static instance: ExecListEntry = ExecListEntry { - exec_id: ::std::string::String::new(), - slot: 0, - state: ::protobuf::EnumOrUnknown::from_i32(0), - create_time_unix: 0, - argv_sha256: ::std::string::String::new(), - stdout_truncated: false, - stderr_truncated: false, - dropped_bytes: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecListEntry { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecListEntry").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecListEntry { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecListEntry { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.WriteStdinRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct WriteStdinRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinRequest.offset) - pub offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinRequest.data) - pub data: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinRequest.close_after) - pub close_after: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinRequest.client_deadline_ms) - pub client_deadline_ms: ::std::option::Option, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.WriteStdinRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a WriteStdinRequest { - fn default() -> &'a WriteStdinRequest { - ::default_instance() - } -} - -impl WriteStdinRequest { - pub fn new() -> WriteStdinRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(5); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &WriteStdinRequest| { &m.metadata }, - |m: &mut WriteStdinRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &WriteStdinRequest| { &m.offset }, - |m: &mut WriteStdinRequest| { &mut m.offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "data", - |m: &WriteStdinRequest| { &m.data }, - |m: &mut WriteStdinRequest| { &mut m.data }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "close_after", - |m: &WriteStdinRequest| { &m.close_after }, - |m: &mut WriteStdinRequest| { &mut m.close_after }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "client_deadline_ms", - |m: &WriteStdinRequest| { &m.client_deadline_ms }, - |m: &mut WriteStdinRequest| { &mut m.client_deadline_ms }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "WriteStdinRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for WriteStdinRequest { - const NAME: &'static str = "WriteStdinRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.offset = is.read_uint64()?; - }, - 26 => { - self.data = is.read_bytes()?; - }, - 32 => { - self.close_after = is.read_bool()?; - }, - 40 => { - self.client_deadline_ms = ::std::option::Option::Some(is.read_uint64()?); - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.offset); - } - if !self.data.is_empty() { - my_size += ::protobuf::rt::bytes_size(3, &self.data); - } - if self.close_after != false { - my_size += 1 + 1; - } - if let Some(v) = self.client_deadline_ms { - my_size += ::protobuf::rt::uint64_size(5, v); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.offset != 0 { - os.write_uint64(2, self.offset)?; - } - if !self.data.is_empty() { - os.write_bytes(3, &self.data)?; - } - if self.close_after != false { - os.write_bool(4, self.close_after)?; - } - if let Some(v) = self.client_deadline_ms { - os.write_uint64(5, v)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> WriteStdinRequest { - WriteStdinRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.offset = 0; - self.data.clear(); - self.close_after = false; - self.client_deadline_ms = ::std::option::Option::None; - self.special_fields.clear(); - } - - fn default_instance() -> &'static WriteStdinRequest { - static instance: WriteStdinRequest = WriteStdinRequest { - metadata: ::protobuf::MessageField::none(), - offset: 0, - data: ::std::vec::Vec::new(), - close_after: false, - client_deadline_ms: ::std::option::Option::None, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for WriteStdinRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("WriteStdinRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for WriteStdinRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for WriteStdinRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.WriteStdinResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct WriteStdinResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinResponse.accepted_offset) - pub accepted_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinResponse.accepted_len) - pub accepted_len: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinResponse.next_offset) - pub next_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinResponse.stdin_state) - pub stdin_state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinResponse.blocked_ms) - pub blocked_ms: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinResponse.disposition) - pub disposition: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.WriteStdinResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.WriteStdinResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a WriteStdinResponse { - fn default() -> &'a WriteStdinResponse { - ::default_instance() - } -} - -impl WriteStdinResponse { - pub fn new() -> WriteStdinResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(7); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "accepted_offset", - |m: &WriteStdinResponse| { &m.accepted_offset }, - |m: &mut WriteStdinResponse| { &mut m.accepted_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "accepted_len", - |m: &WriteStdinResponse| { &m.accepted_len }, - |m: &mut WriteStdinResponse| { &mut m.accepted_len }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "next_offset", - |m: &WriteStdinResponse| { &m.next_offset }, - |m: &mut WriteStdinResponse| { &mut m.next_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdin_state", - |m: &WriteStdinResponse| { &m.stdin_state }, - |m: &mut WriteStdinResponse| { &mut m.stdin_state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "blocked_ms", - |m: &WriteStdinResponse| { &m.blocked_ms }, - |m: &mut WriteStdinResponse| { &mut m.blocked_ms }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "disposition", - |m: &WriteStdinResponse| { &m.disposition }, - |m: &mut WriteStdinResponse| { &mut m.disposition }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &WriteStdinResponse| { &m.error }, - |m: &mut WriteStdinResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "WriteStdinResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for WriteStdinResponse { - const NAME: &'static str = "WriteStdinResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.accepted_offset = is.read_uint64()?; - }, - 16 => { - self.accepted_len = is.read_uint64()?; - }, - 24 => { - self.next_offset = is.read_uint64()?; - }, - 32 => { - self.stdin_state = is.read_enum_or_unknown()?; - }, - 40 => { - self.blocked_ms = is.read_uint64()?; - }, - 48 => { - self.disposition = is.read_enum_or_unknown()?; - }, - 58 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.accepted_offset != 0 { - my_size += ::protobuf::rt::uint64_size(1, self.accepted_offset); - } - if self.accepted_len != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.accepted_len); - } - if self.next_offset != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.next_offset); - } - if self.stdin_state != ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(4, self.stdin_state.value()); - } - if self.blocked_ms != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.blocked_ms); - } - if self.disposition != ::protobuf::EnumOrUnknown::new(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(6, self.disposition.value()); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.accepted_offset != 0 { - os.write_uint64(1, self.accepted_offset)?; - } - if self.accepted_len != 0 { - os.write_uint64(2, self.accepted_len)?; - } - if self.next_offset != 0 { - os.write_uint64(3, self.next_offset)?; - } - if self.stdin_state != ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED) { - os.write_enum(4, ::protobuf::EnumOrUnknown::value(&self.stdin_state))?; - } - if self.blocked_ms != 0 { - os.write_uint64(5, self.blocked_ms)?; - } - if self.disposition != ::protobuf::EnumOrUnknown::new(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED) { - os.write_enum(6, ::protobuf::EnumOrUnknown::value(&self.disposition))?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(7, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> WriteStdinResponse { - WriteStdinResponse::new() - } - - fn clear(&mut self) { - self.accepted_offset = 0; - self.accepted_len = 0; - self.next_offset = 0; - self.stdin_state = ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED); - self.blocked_ms = 0; - self.disposition = ::protobuf::EnumOrUnknown::new(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static WriteStdinResponse { - static instance: WriteStdinResponse = WriteStdinResponse { - accepted_offset: 0, - accepted_len: 0, - next_offset: 0, - stdin_state: ::protobuf::EnumOrUnknown::from_i32(0), - blocked_ms: 0, - disposition: ::protobuf::EnumOrUnknown::from_i32(0), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for WriteStdinResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("WriteStdinResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for WriteStdinResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for WriteStdinResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ReadOutputRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ReadOutputRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputRequest.stream) - pub stream: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputRequest.offset) - pub offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputRequest.max_len) - pub max_len: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputRequest.wait) - pub wait: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputRequest.timeout_ms) - pub timeout_ms: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ReadOutputRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ReadOutputRequest { - fn default() -> &'a ReadOutputRequest { - ::default_instance() - } -} - -impl ReadOutputRequest { - pub fn new() -> ReadOutputRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(6); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &ReadOutputRequest| { &m.metadata }, - |m: &mut ReadOutputRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stream", - |m: &ReadOutputRequest| { &m.stream }, - |m: &mut ReadOutputRequest| { &mut m.stream }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &ReadOutputRequest| { &m.offset }, - |m: &mut ReadOutputRequest| { &mut m.offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_len", - |m: &ReadOutputRequest| { &m.max_len }, - |m: &mut ReadOutputRequest| { &mut m.max_len }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "wait", - |m: &ReadOutputRequest| { &m.wait }, - |m: &mut ReadOutputRequest| { &mut m.wait }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "timeout_ms", - |m: &ReadOutputRequest| { &m.timeout_ms }, - |m: &mut ReadOutputRequest| { &mut m.timeout_ms }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ReadOutputRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ReadOutputRequest { - const NAME: &'static str = "ReadOutputRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.stream = is.read_enum_or_unknown()?; - }, - 24 => { - self.offset = is.read_uint64()?; - }, - 32 => { - self.max_len = is.read_uint64()?; - }, - 40 => { - self.wait = is.read_bool()?; - }, - 48 => { - self.timeout_ms = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.stream.value()); - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.offset); - } - if self.max_len != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.max_len); - } - if self.wait != false { - my_size += 1 + 1; - } - if self.timeout_ms != 0 { - my_size += ::protobuf::rt::uint64_size(6, self.timeout_ms); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.stream))?; - } - if self.offset != 0 { - os.write_uint64(3, self.offset)?; - } - if self.max_len != 0 { - os.write_uint64(4, self.max_len)?; - } - if self.wait != false { - os.write_bool(5, self.wait)?; - } - if self.timeout_ms != 0 { - os.write_uint64(6, self.timeout_ms)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ReadOutputRequest { - ReadOutputRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.stream = ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED); - self.offset = 0; - self.max_len = 0; - self.wait = false; - self.timeout_ms = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static ReadOutputRequest { - static instance: ReadOutputRequest = ReadOutputRequest { - metadata: ::protobuf::MessageField::none(), - stream: ::protobuf::EnumOrUnknown::from_i32(0), - offset: 0, - max_len: 0, - wait: false, - timeout_ms: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ReadOutputRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ReadOutputRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ReadOutputRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ReadOutputRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ReadOutputResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ReadOutputResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.stream) - pub stream: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.offset) - pub offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.end_offset) - pub end_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.data) - pub data: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.next_offset) - pub next_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.eof) - pub eof: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.start_offset) - pub start_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.dropped_bytes) - pub dropped_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.truncated) - pub truncated: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.timed_out) - pub timed_out: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadOutputResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ReadOutputResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ReadOutputResponse { - fn default() -> &'a ReadOutputResponse { - ::default_instance() - } -} - -impl ReadOutputResponse { - pub fn new() -> ReadOutputResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(11); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stream", - |m: &ReadOutputResponse| { &m.stream }, - |m: &mut ReadOutputResponse| { &mut m.stream }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &ReadOutputResponse| { &m.offset }, - |m: &mut ReadOutputResponse| { &mut m.offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "end_offset", - |m: &ReadOutputResponse| { &m.end_offset }, - |m: &mut ReadOutputResponse| { &mut m.end_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "data", - |m: &ReadOutputResponse| { &m.data }, - |m: &mut ReadOutputResponse| { &mut m.data }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "next_offset", - |m: &ReadOutputResponse| { &m.next_offset }, - |m: &mut ReadOutputResponse| { &mut m.next_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "eof", - |m: &ReadOutputResponse| { &m.eof }, - |m: &mut ReadOutputResponse| { &mut m.eof }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "start_offset", - |m: &ReadOutputResponse| { &m.start_offset }, - |m: &mut ReadOutputResponse| { &mut m.start_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "dropped_bytes", - |m: &ReadOutputResponse| { &m.dropped_bytes }, - |m: &mut ReadOutputResponse| { &mut m.dropped_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "truncated", - |m: &ReadOutputResponse| { &m.truncated }, - |m: &mut ReadOutputResponse| { &mut m.truncated }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "timed_out", - |m: &ReadOutputResponse| { &m.timed_out }, - |m: &mut ReadOutputResponse| { &mut m.timed_out }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ReadOutputResponse| { &m.error }, - |m: &mut ReadOutputResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ReadOutputResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ReadOutputResponse { - const NAME: &'static str = "ReadOutputResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.stream = is.read_enum_or_unknown()?; - }, - 16 => { - self.offset = is.read_uint64()?; - }, - 24 => { - self.end_offset = is.read_uint64()?; - }, - 34 => { - self.data = is.read_bytes()?; - }, - 40 => { - self.next_offset = is.read_uint64()?; - }, - 48 => { - self.eof = is.read_bool()?; - }, - 56 => { - self.start_offset = is.read_uint64()?; - }, - 64 => { - self.dropped_bytes = is.read_uint64()?; - }, - 72 => { - self.truncated = is.read_bool()?; - }, - 80 => { - self.timed_out = is.read_bool()?; - }, - 90 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.stream.value()); - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.offset); - } - if self.end_offset != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.end_offset); - } - if !self.data.is_empty() { - my_size += ::protobuf::rt::bytes_size(4, &self.data); - } - if self.next_offset != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.next_offset); - } - if self.eof != false { - my_size += 1 + 1; - } - if self.start_offset != 0 { - my_size += ::protobuf::rt::uint64_size(7, self.start_offset); - } - if self.dropped_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(8, self.dropped_bytes); - } - if self.truncated != false { - my_size += 1 + 1; - } - if self.timed_out != false { - my_size += 1 + 1; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.stream))?; - } - if self.offset != 0 { - os.write_uint64(2, self.offset)?; - } - if self.end_offset != 0 { - os.write_uint64(3, self.end_offset)?; - } - if !self.data.is_empty() { - os.write_bytes(4, &self.data)?; - } - if self.next_offset != 0 { - os.write_uint64(5, self.next_offset)?; - } - if self.eof != false { - os.write_bool(6, self.eof)?; - } - if self.start_offset != 0 { - os.write_uint64(7, self.start_offset)?; - } - if self.dropped_bytes != 0 { - os.write_uint64(8, self.dropped_bytes)?; - } - if self.truncated != false { - os.write_bool(9, self.truncated)?; - } - if self.timed_out != false { - os.write_bool(10, self.timed_out)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(11, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ReadOutputResponse { - ReadOutputResponse::new() - } - - fn clear(&mut self) { - self.stream = ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED); - self.offset = 0; - self.end_offset = 0; - self.data.clear(); - self.next_offset = 0; - self.eof = false; - self.start_offset = 0; - self.dropped_bytes = 0; - self.truncated = false; - self.timed_out = false; - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ReadOutputResponse { - static instance: ReadOutputResponse = ReadOutputResponse { - stream: ::protobuf::EnumOrUnknown::from_i32(0), - offset: 0, - end_offset: 0, - data: ::std::vec::Vec::new(), - next_offset: 0, - eof: false, - start_offset: 0, - dropped_bytes: 0, - truncated: false, - timed_out: false, - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ReadOutputResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ReadOutputResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ReadOutputResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ReadOutputResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.CloseStdinRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct CloseStdinRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.CloseStdinRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.CloseStdinRequest.offset) - pub offset: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.CloseStdinRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a CloseStdinRequest { - fn default() -> &'a CloseStdinRequest { - ::default_instance() - } -} - -impl CloseStdinRequest { - pub fn new() -> CloseStdinRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &CloseStdinRequest| { &m.metadata }, - |m: &mut CloseStdinRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &CloseStdinRequest| { &m.offset }, - |m: &mut CloseStdinRequest| { &mut m.offset }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "CloseStdinRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for CloseStdinRequest { - const NAME: &'static str = "CloseStdinRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.offset = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.offset); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.offset != 0 { - os.write_uint64(2, self.offset)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> CloseStdinRequest { - CloseStdinRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.offset = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static CloseStdinRequest { - static instance: CloseStdinRequest = CloseStdinRequest { - metadata: ::protobuf::MessageField::none(), - offset: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for CloseStdinRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("CloseStdinRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for CloseStdinRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for CloseStdinRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.CloseStdinResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct CloseStdinResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.CloseStdinResponse.stdin_state) - pub stdin_state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.CloseStdinResponse.final_offset) - pub final_offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.CloseStdinResponse.disposition) - pub disposition: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.CloseStdinResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.CloseStdinResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a CloseStdinResponse { - fn default() -> &'a CloseStdinResponse { - ::default_instance() - } -} - -impl CloseStdinResponse { - pub fn new() -> CloseStdinResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stdin_state", - |m: &CloseStdinResponse| { &m.stdin_state }, - |m: &mut CloseStdinResponse| { &mut m.stdin_state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "final_offset", - |m: &CloseStdinResponse| { &m.final_offset }, - |m: &mut CloseStdinResponse| { &mut m.final_offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "disposition", - |m: &CloseStdinResponse| { &m.disposition }, - |m: &mut CloseStdinResponse| { &mut m.disposition }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &CloseStdinResponse| { &m.error }, - |m: &mut CloseStdinResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "CloseStdinResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for CloseStdinResponse { - const NAME: &'static str = "CloseStdinResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.stdin_state = is.read_enum_or_unknown()?; - }, - 16 => { - self.final_offset = is.read_uint64()?; - }, - 24 => { - self.disposition = is.read_enum_or_unknown()?; - }, - 34 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.stdin_state != ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.stdin_state.value()); - } - if self.final_offset != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.final_offset); - } - if self.disposition != ::protobuf::EnumOrUnknown::new(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.disposition.value()); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.stdin_state != ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.stdin_state))?; - } - if self.final_offset != 0 { - os.write_uint64(2, self.final_offset)?; - } - if self.disposition != ::protobuf::EnumOrUnknown::new(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.disposition))?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> CloseStdinResponse { - CloseStdinResponse::new() - } - - fn clear(&mut self) { - self.stdin_state = ::protobuf::EnumOrUnknown::new(StdinState::STDIN_STATE_UNSPECIFIED); - self.final_offset = 0; - self.disposition = ::protobuf::EnumOrUnknown::new(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static CloseStdinResponse { - static instance: CloseStdinResponse = CloseStdinResponse { - stdin_state: ::protobuf::EnumOrUnknown::from_i32(0), - final_offset: 0, - disposition: ::protobuf::EnumOrUnknown::from_i32(0), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for CloseStdinResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("CloseStdinResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for CloseStdinResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for CloseStdinResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TtyWinResizeRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TtyWinResizeRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.TtyWinResizeRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.TtyWinResizeRequest.control_seq) - pub control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.TtyWinResizeRequest.rows) - pub rows: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.TtyWinResizeRequest.cols) - pub cols: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TtyWinResizeRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TtyWinResizeRequest { - fn default() -> &'a TtyWinResizeRequest { - ::default_instance() - } -} - -impl TtyWinResizeRequest { - pub fn new() -> TtyWinResizeRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &TtyWinResizeRequest| { &m.metadata }, - |m: &mut TtyWinResizeRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "control_seq", - |m: &TtyWinResizeRequest| { &m.control_seq }, - |m: &mut TtyWinResizeRequest| { &mut m.control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "rows", - |m: &TtyWinResizeRequest| { &m.rows }, - |m: &mut TtyWinResizeRequest| { &mut m.rows }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "cols", - |m: &TtyWinResizeRequest| { &m.cols }, - |m: &mut TtyWinResizeRequest| { &mut m.cols }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TtyWinResizeRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TtyWinResizeRequest { - const NAME: &'static str = "TtyWinResizeRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.control_seq = is.read_uint64()?; - }, - 24 => { - self.rows = is.read_uint32()?; - }, - 32 => { - self.cols = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.control_seq); - } - if self.rows != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.rows); - } - if self.cols != 0 { - my_size += ::protobuf::rt::uint32_size(4, self.cols); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.control_seq != 0 { - os.write_uint64(2, self.control_seq)?; - } - if self.rows != 0 { - os.write_uint32(3, self.rows)?; - } - if self.cols != 0 { - os.write_uint32(4, self.cols)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TtyWinResizeRequest { - TtyWinResizeRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.control_seq = 0; - self.rows = 0; - self.cols = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static TtyWinResizeRequest { - static instance: TtyWinResizeRequest = TtyWinResizeRequest { - metadata: ::protobuf::MessageField::none(), - control_seq: 0, - rows: 0, - cols: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TtyWinResizeRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TtyWinResizeRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TtyWinResizeRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TtyWinResizeRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecSignalRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecSignalRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecSignalRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecSignalRequest.control_seq) - pub control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecSignalRequest.signal) - pub signal: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecSignalRequest.target) - pub target: ::protobuf::EnumOrUnknown, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecSignalRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecSignalRequest { - fn default() -> &'a ExecSignalRequest { - ::default_instance() - } -} - -impl ExecSignalRequest { - pub fn new() -> ExecSignalRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &ExecSignalRequest| { &m.metadata }, - |m: &mut ExecSignalRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "control_seq", - |m: &ExecSignalRequest| { &m.control_seq }, - |m: &mut ExecSignalRequest| { &mut m.control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "signal", - |m: &ExecSignalRequest| { &m.signal }, - |m: &mut ExecSignalRequest| { &mut m.signal }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "target", - |m: &ExecSignalRequest| { &m.target }, - |m: &mut ExecSignalRequest| { &mut m.target }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecSignalRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecSignalRequest { - const NAME: &'static str = "ExecSignalRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.control_seq = is.read_uint64()?; - }, - 24 => { - self.signal = is.read_uint32()?; - }, - 32 => { - self.target = is.read_enum_or_unknown()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.control_seq); - } - if self.signal != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.signal); - } - if self.target != ::protobuf::EnumOrUnknown::new(SignalTarget::SIGNAL_TARGET_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(4, self.target.value()); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.control_seq != 0 { - os.write_uint64(2, self.control_seq)?; - } - if self.signal != 0 { - os.write_uint32(3, self.signal)?; - } - if self.target != ::protobuf::EnumOrUnknown::new(SignalTarget::SIGNAL_TARGET_UNSPECIFIED) { - os.write_enum(4, ::protobuf::EnumOrUnknown::value(&self.target))?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecSignalRequest { - ExecSignalRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.control_seq = 0; - self.signal = 0; - self.target = ::protobuf::EnumOrUnknown::new(SignalTarget::SIGNAL_TARGET_UNSPECIFIED); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecSignalRequest { - static instance: ExecSignalRequest = ExecSignalRequest { - metadata: ::protobuf::MessageField::none(), - control_seq: 0, - signal: 0, - target: ::protobuf::EnumOrUnknown::from_i32(0), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecSignalRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecSignalRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecSignalRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecSignalRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ExecCancelRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ExecCancelRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCancelRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCancelRequest.control_seq) - pub control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ExecCancelRequest.reason) - pub reason: ::protobuf::EnumOrUnknown, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ExecCancelRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ExecCancelRequest { - fn default() -> &'a ExecCancelRequest { - ::default_instance() - } -} - -impl ExecCancelRequest { - pub fn new() -> ExecCancelRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, ExecRequestMetadata>( - "metadata", - |m: &ExecCancelRequest| { &m.metadata }, - |m: &mut ExecCancelRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "control_seq", - |m: &ExecCancelRequest| { &m.control_seq }, - |m: &mut ExecCancelRequest| { &mut m.control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "reason", - |m: &ExecCancelRequest| { &m.reason }, - |m: &mut ExecCancelRequest| { &mut m.reason }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ExecCancelRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ExecCancelRequest { - const NAME: &'static str = "ExecCancelRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.control_seq = is.read_uint64()?; - }, - 24 => { - self.reason = is.read_enum_or_unknown()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.control_seq); - } - if self.reason != ::protobuf::EnumOrUnknown::new(ExecCancelReason::EXEC_CANCEL_REASON_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.reason.value()); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.control_seq != 0 { - os.write_uint64(2, self.control_seq)?; - } - if self.reason != ::protobuf::EnumOrUnknown::new(ExecCancelReason::EXEC_CANCEL_REASON_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.reason))?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ExecCancelRequest { - ExecCancelRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.control_seq = 0; - self.reason = ::protobuf::EnumOrUnknown::new(ExecCancelReason::EXEC_CANCEL_REASON_UNSPECIFIED); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ExecCancelRequest { - static instance: ExecCancelRequest = ExecCancelRequest { - metadata: ::protobuf::MessageField::none(), - control_seq: 0, - reason: ::protobuf::EnumOrUnknown::from_i32(0), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ExecCancelRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ExecCancelRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ExecCancelRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ExecCancelRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ControlAck) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ControlAck { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ControlAck.control_seq) - pub control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ControlAck.duplicate) - pub duplicate: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ControlAck.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ControlAck.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ControlAck { - fn default() -> &'a ControlAck { - ::default_instance() - } -} - -impl ControlAck { - pub fn new() -> ControlAck { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "control_seq", - |m: &ControlAck| { &m.control_seq }, - |m: &mut ControlAck| { &mut m.control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "duplicate", - |m: &ControlAck| { &m.duplicate }, - |m: &mut ControlAck| { &mut m.duplicate }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ControlAck| { &m.error }, - |m: &mut ControlAck| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ControlAck", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ControlAck { - const NAME: &'static str = "ControlAck"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.control_seq = is.read_uint64()?; - }, - 16 => { - self.duplicate = is.read_bool()?; - }, - 26 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(1, self.control_seq); - } - if self.duplicate != false { - my_size += 1 + 1; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.control_seq != 0 { - os.write_uint64(1, self.control_seq)?; - } - if self.duplicate != false { - os.write_bool(2, self.duplicate)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ControlAck { - ControlAck::new() - } - - fn clear(&mut self) { - self.control_seq = 0; - self.duplicate = false; - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ControlAck { - static instance: ControlAck = ControlAck { - control_seq: 0, - duplicate: false, - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ControlAck { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ControlAck").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ControlAck { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ControlAck { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ReadGuestFileRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ReadGuestFileRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ReadGuestFileRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadGuestFileRequest.file_id) - pub file_id: ::protobuf::EnumOrUnknown, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ReadGuestFileRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ReadGuestFileRequest { - fn default() -> &'a ReadGuestFileRequest { - ::default_instance() - } -} - -impl ReadGuestFileRequest { - pub fn new() -> ReadGuestFileRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &ReadGuestFileRequest| { &m.metadata }, - |m: &mut ReadGuestFileRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "file_id", - |m: &ReadGuestFileRequest| { &m.file_id }, - |m: &mut ReadGuestFileRequest| { &mut m.file_id }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ReadGuestFileRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ReadGuestFileRequest { - const NAME: &'static str = "ReadGuestFileRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.file_id = is.read_enum_or_unknown()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.file_id != ::protobuf::EnumOrUnknown::new(GuestFileId::GUEST_FILE_ID_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.file_id.value()); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.file_id != ::protobuf::EnumOrUnknown::new(GuestFileId::GUEST_FILE_ID_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.file_id))?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ReadGuestFileRequest { - ReadGuestFileRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.file_id = ::protobuf::EnumOrUnknown::new(GuestFileId::GUEST_FILE_ID_UNSPECIFIED); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ReadGuestFileRequest { - static instance: ReadGuestFileRequest = ReadGuestFileRequest { - metadata: ::protobuf::MessageField::none(), - file_id: ::protobuf::EnumOrUnknown::from_i32(0), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ReadGuestFileRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ReadGuestFileRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ReadGuestFileRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ReadGuestFileRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ReadGuestFileResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ReadGuestFileResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ReadGuestFileResponse.file_id) - pub file_id: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadGuestFileResponse.size_bytes) - pub size_bytes: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadGuestFileResponse.content) - pub content: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadGuestFileResponse.sha256) - pub sha256: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ReadGuestFileResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ReadGuestFileResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ReadGuestFileResponse { - fn default() -> &'a ReadGuestFileResponse { - ::default_instance() - } -} - -impl ReadGuestFileResponse { - pub fn new() -> ReadGuestFileResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(5); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "file_id", - |m: &ReadGuestFileResponse| { &m.file_id }, - |m: &mut ReadGuestFileResponse| { &mut m.file_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "size_bytes", - |m: &ReadGuestFileResponse| { &m.size_bytes }, - |m: &mut ReadGuestFileResponse| { &mut m.size_bytes }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "content", - |m: &ReadGuestFileResponse| { &m.content }, - |m: &mut ReadGuestFileResponse| { &mut m.content }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "sha256", - |m: &ReadGuestFileResponse| { &m.sha256 }, - |m: &mut ReadGuestFileResponse| { &mut m.sha256 }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ReadGuestFileResponse| { &m.error }, - |m: &mut ReadGuestFileResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ReadGuestFileResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ReadGuestFileResponse { - const NAME: &'static str = "ReadGuestFileResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.file_id = is.read_enum_or_unknown()?; - }, - 16 => { - self.size_bytes = is.read_uint64()?; - }, - 26 => { - self.content = is.read_bytes()?; - }, - 34 => { - self.sha256 = is.read_string()?; - }, - 42 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.file_id != ::protobuf::EnumOrUnknown::new(GuestFileId::GUEST_FILE_ID_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.file_id.value()); - } - if self.size_bytes != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.size_bytes); - } - if !self.content.is_empty() { - my_size += ::protobuf::rt::bytes_size(3, &self.content); - } - if !self.sha256.is_empty() { - my_size += ::protobuf::rt::string_size(4, &self.sha256); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.file_id != ::protobuf::EnumOrUnknown::new(GuestFileId::GUEST_FILE_ID_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.file_id))?; - } - if self.size_bytes != 0 { - os.write_uint64(2, self.size_bytes)?; - } - if !self.content.is_empty() { - os.write_bytes(3, &self.content)?; - } - if !self.sha256.is_empty() { - os.write_string(4, &self.sha256)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(5, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ReadGuestFileResponse { - ReadGuestFileResponse::new() - } - - fn clear(&mut self) { - self.file_id = ::protobuf::EnumOrUnknown::new(GuestFileId::GUEST_FILE_ID_UNSPECIFIED); - self.size_bytes = 0; - self.content.clear(); - self.sha256.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ReadGuestFileResponse { - static instance: ReadGuestFileResponse = ReadGuestFileResponse { - file_id: ::protobuf::EnumOrUnknown::from_i32(0), - size_bytes: 0, - content: ::std::vec::Vec::new(), - sha256: ::std::string::String::new(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ReadGuestFileResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ReadGuestFileResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ReadGuestFileResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ReadGuestFileResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.UsbipImportRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct UsbipImportRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportRequest.action) - pub action: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportRequest.host) - pub host: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportRequest.bus_id) - pub bus_id: ::std::string::String, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.UsbipImportRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a UsbipImportRequest { - fn default() -> &'a UsbipImportRequest { - ::default_instance() - } -} - -impl UsbipImportRequest { - pub fn new() -> UsbipImportRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &UsbipImportRequest| { &m.metadata }, - |m: &mut UsbipImportRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "action", - |m: &UsbipImportRequest| { &m.action }, - |m: &mut UsbipImportRequest| { &mut m.action }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "host", - |m: &UsbipImportRequest| { &m.host }, - |m: &mut UsbipImportRequest| { &mut m.host }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "bus_id", - |m: &UsbipImportRequest| { &m.bus_id }, - |m: &mut UsbipImportRequest| { &mut m.bus_id }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "UsbipImportRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for UsbipImportRequest { - const NAME: &'static str = "UsbipImportRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.action = is.read_enum_or_unknown()?; - }, - 26 => { - self.host = is.read_string()?; - }, - 34 => { - self.bus_id = is.read_string()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.action != ::protobuf::EnumOrUnknown::new(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.action.value()); - } - if !self.host.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.host); - } - if !self.bus_id.is_empty() { - my_size += ::protobuf::rt::string_size(4, &self.bus_id); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.action != ::protobuf::EnumOrUnknown::new(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.action))?; - } - if !self.host.is_empty() { - os.write_string(3, &self.host)?; - } - if !self.bus_id.is_empty() { - os.write_string(4, &self.bus_id)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> UsbipImportRequest { - UsbipImportRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.action = ::protobuf::EnumOrUnknown::new(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED); - self.host.clear(); - self.bus_id.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static UsbipImportRequest { - static instance: UsbipImportRequest = UsbipImportRequest { - metadata: ::protobuf::MessageField::none(), - action: ::protobuf::EnumOrUnknown::from_i32(0), - host: ::std::string::String::new(), - bus_id: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for UsbipImportRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("UsbipImportRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for UsbipImportRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for UsbipImportRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.UsbipImportResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct UsbipImportResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportResponse.action) - pub action: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportResponse.bus_id) - pub bus_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportResponse.detached_ports) - pub detached_ports: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipImportResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.UsbipImportResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a UsbipImportResponse { - fn default() -> &'a UsbipImportResponse { - ::default_instance() - } -} - -impl UsbipImportResponse { - pub fn new() -> UsbipImportResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "action", - |m: &UsbipImportResponse| { &m.action }, - |m: &mut UsbipImportResponse| { &mut m.action }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "bus_id", - |m: &UsbipImportResponse| { &m.bus_id }, - |m: &mut UsbipImportResponse| { &mut m.bus_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "detached_ports", - |m: &UsbipImportResponse| { &m.detached_ports }, - |m: &mut UsbipImportResponse| { &mut m.detached_ports }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &UsbipImportResponse| { &m.error }, - |m: &mut UsbipImportResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "UsbipImportResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for UsbipImportResponse { - const NAME: &'static str = "UsbipImportResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.action = is.read_enum_or_unknown()?; - }, - 18 => { - self.bus_id = is.read_string()?; - }, - 24 => { - self.detached_ports = is.read_uint32()?; - }, - 34 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.action != ::protobuf::EnumOrUnknown::new(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.action.value()); - } - if !self.bus_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.bus_id); - } - if self.detached_ports != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.detached_ports); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.action != ::protobuf::EnumOrUnknown::new(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.action))?; - } - if !self.bus_id.is_empty() { - os.write_string(2, &self.bus_id)?; - } - if self.detached_ports != 0 { - os.write_uint32(3, self.detached_ports)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> UsbipImportResponse { - UsbipImportResponse::new() - } - - fn clear(&mut self) { - self.action = ::protobuf::EnumOrUnknown::new(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED); - self.bus_id.clear(); - self.detached_ports = 0; - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static UsbipImportResponse { - static instance: UsbipImportResponse = UsbipImportResponse { - action: ::protobuf::EnumOrUnknown::from_i32(0), - bus_id: ::std::string::String::new(), - detached_ports: 0, - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for UsbipImportResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("UsbipImportResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for UsbipImportResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for UsbipImportResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.UsbipStatusRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct UsbipStatusRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusRequest.host) - pub host: ::std::option::Option<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusRequest.bus_id) - pub bus_id: ::std::option::Option<::std::string::String>, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.UsbipStatusRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a UsbipStatusRequest { - fn default() -> &'a UsbipStatusRequest { - ::default_instance() - } -} - -impl UsbipStatusRequest { - pub fn new() -> UsbipStatusRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &UsbipStatusRequest| { &m.metadata }, - |m: &mut UsbipStatusRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "host", - |m: &UsbipStatusRequest| { &m.host }, - |m: &mut UsbipStatusRequest| { &mut m.host }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "bus_id", - |m: &UsbipStatusRequest| { &m.bus_id }, - |m: &mut UsbipStatusRequest| { &mut m.bus_id }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "UsbipStatusRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for UsbipStatusRequest { - const NAME: &'static str = "UsbipStatusRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.host = ::std::option::Option::Some(is.read_string()?); - }, - 26 => { - self.bus_id = ::std::option::Option::Some(is.read_string()?); - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.host.as_ref() { - my_size += ::protobuf::rt::string_size(2, &v); - } - if let Some(v) = self.bus_id.as_ref() { - my_size += ::protobuf::rt::string_size(3, &v); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if let Some(v) = self.host.as_ref() { - os.write_string(2, v)?; - } - if let Some(v) = self.bus_id.as_ref() { - os.write_string(3, v)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> UsbipStatusRequest { - UsbipStatusRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.host = ::std::option::Option::None; - self.bus_id = ::std::option::Option::None; - self.special_fields.clear(); - } - - fn default_instance() -> &'static UsbipStatusRequest { - static instance: UsbipStatusRequest = UsbipStatusRequest { - metadata: ::protobuf::MessageField::none(), - host: ::std::option::Option::None, - bus_id: ::std::option::Option::None, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for UsbipStatusRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("UsbipStatusRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for UsbipStatusRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for UsbipStatusRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.UsbipStatusResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct UsbipStatusResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusResponse.imports) - pub imports: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.UsbipStatusResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a UsbipStatusResponse { - fn default() -> &'a UsbipStatusResponse { - ::default_instance() - } -} - -impl UsbipStatusResponse { - pub fn new() -> UsbipStatusResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "imports", - |m: &UsbipStatusResponse| { &m.imports }, - |m: &mut UsbipStatusResponse| { &mut m.imports }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &UsbipStatusResponse| { &m.error }, - |m: &mut UsbipStatusResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "UsbipStatusResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for UsbipStatusResponse { - const NAME: &'static str = "UsbipStatusResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.imports.push(is.read_message()?); - }, - 18 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - for value in &self.imports { - let len = value.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - }; - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - for v in &self.imports { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - }; - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> UsbipStatusResponse { - UsbipStatusResponse::new() - } - - fn clear(&mut self) { - self.imports.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static UsbipStatusResponse { - static instance: UsbipStatusResponse = UsbipStatusResponse { - imports: ::std::vec::Vec::new(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for UsbipStatusResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("UsbipStatusResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for UsbipStatusResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for UsbipStatusResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.UsbipStatusEntry) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct UsbipStatusEntry { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusEntry.port) - pub port: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusEntry.host) - pub host: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusEntry.tcp_port) - pub tcp_port: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.UsbipStatusEntry.bus_id) - pub bus_id: ::std::string::String, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.UsbipStatusEntry.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a UsbipStatusEntry { - fn default() -> &'a UsbipStatusEntry { - ::default_instance() - } -} - -impl UsbipStatusEntry { - pub fn new() -> UsbipStatusEntry { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "port", - |m: &UsbipStatusEntry| { &m.port }, - |m: &mut UsbipStatusEntry| { &mut m.port }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "host", - |m: &UsbipStatusEntry| { &m.host }, - |m: &mut UsbipStatusEntry| { &mut m.host }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "tcp_port", - |m: &UsbipStatusEntry| { &m.tcp_port }, - |m: &mut UsbipStatusEntry| { &mut m.tcp_port }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "bus_id", - |m: &UsbipStatusEntry| { &m.bus_id }, - |m: &mut UsbipStatusEntry| { &mut m.bus_id }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "UsbipStatusEntry", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for UsbipStatusEntry { - const NAME: &'static str = "UsbipStatusEntry"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.port = is.read_uint32()?; - }, - 18 => { - self.host = is.read_string()?; - }, - 24 => { - self.tcp_port = is.read_uint32()?; - }, - 34 => { - self.bus_id = is.read_string()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.port != 0 { - my_size += ::protobuf::rt::uint32_size(1, self.port); - } - if !self.host.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.host); - } - if self.tcp_port != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.tcp_port); - } - if !self.bus_id.is_empty() { - my_size += ::protobuf::rt::string_size(4, &self.bus_id); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.port != 0 { - os.write_uint32(1, self.port)?; - } - if !self.host.is_empty() { - os.write_string(2, &self.host)?; - } - if self.tcp_port != 0 { - os.write_uint32(3, self.tcp_port)?; - } - if !self.bus_id.is_empty() { - os.write_string(4, &self.bus_id)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> UsbipStatusEntry { - UsbipStatusEntry::new() - } - - fn clear(&mut self) { - self.port = 0; - self.host.clear(); - self.tcp_port = 0; - self.bus_id.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static UsbipStatusEntry { - static instance: UsbipStatusEntry = UsbipStatusEntry { - port: 0, - host: ::std::string::String::new(), - tcp_port: 0, - bus_id: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for UsbipStatusEntry { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("UsbipStatusEntry").unwrap()).clone() - } -} - -impl ::std::fmt::Display for UsbipStatusEntry { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for UsbipStatusEntry { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.GuestActivationStartRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct GuestActivationStartRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartRequest.activation_id) - pub activation_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartRequest.switch_script_path) - pub switch_script_path: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartRequest.mode) - pub mode: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartRequest.timeout_ms) - pub timeout_ms: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.GuestActivationStartRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a GuestActivationStartRequest { - fn default() -> &'a GuestActivationStartRequest { - ::default_instance() - } -} - -impl GuestActivationStartRequest { - pub fn new() -> GuestActivationStartRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(5); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &GuestActivationStartRequest| { &m.metadata }, - |m: &mut GuestActivationStartRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "activation_id", - |m: &GuestActivationStartRequest| { &m.activation_id }, - |m: &mut GuestActivationStartRequest| { &mut m.activation_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "switch_script_path", - |m: &GuestActivationStartRequest| { &m.switch_script_path }, - |m: &mut GuestActivationStartRequest| { &mut m.switch_script_path }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "mode", - |m: &GuestActivationStartRequest| { &m.mode }, - |m: &mut GuestActivationStartRequest| { &mut m.mode }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "timeout_ms", - |m: &GuestActivationStartRequest| { &m.timeout_ms }, - |m: &mut GuestActivationStartRequest| { &mut m.timeout_ms }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "GuestActivationStartRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for GuestActivationStartRequest { - const NAME: &'static str = "GuestActivationStartRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.activation_id = is.read_string()?; - }, - 26 => { - self.switch_script_path = is.read_string()?; - }, - 32 => { - self.mode = is.read_enum_or_unknown()?; - }, - 40 => { - self.timeout_ms = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.activation_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.activation_id); - } - if !self.switch_script_path.is_empty() { - my_size += ::protobuf::rt::string_size(3, &self.switch_script_path); - } - if self.mode != ::protobuf::EnumOrUnknown::new(GuestActivationMode::GUEST_ACTIVATION_MODE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(4, self.mode.value()); - } - if self.timeout_ms != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.timeout_ms); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.activation_id.is_empty() { - os.write_string(2, &self.activation_id)?; - } - if !self.switch_script_path.is_empty() { - os.write_string(3, &self.switch_script_path)?; - } - if self.mode != ::protobuf::EnumOrUnknown::new(GuestActivationMode::GUEST_ACTIVATION_MODE_UNSPECIFIED) { - os.write_enum(4, ::protobuf::EnumOrUnknown::value(&self.mode))?; - } - if self.timeout_ms != 0 { - os.write_uint64(5, self.timeout_ms)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> GuestActivationStartRequest { - GuestActivationStartRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.activation_id.clear(); - self.switch_script_path.clear(); - self.mode = ::protobuf::EnumOrUnknown::new(GuestActivationMode::GUEST_ACTIVATION_MODE_UNSPECIFIED); - self.timeout_ms = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static GuestActivationStartRequest { - static instance: GuestActivationStartRequest = GuestActivationStartRequest { - metadata: ::protobuf::MessageField::none(), - activation_id: ::std::string::String::new(), - switch_script_path: ::std::string::String::new(), - mode: ::protobuf::EnumOrUnknown::from_i32(0), - timeout_ms: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for GuestActivationStartRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("GuestActivationStartRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for GuestActivationStartRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for GuestActivationStartRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.GuestActivationStartResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct GuestActivationStartResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartResponse.activation_id) - pub activation_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStartResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.GuestActivationStartResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a GuestActivationStartResponse { - fn default() -> &'a GuestActivationStartResponse { - ::default_instance() - } -} - -impl GuestActivationStartResponse { - pub fn new() -> GuestActivationStartResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "activation_id", - |m: &GuestActivationStartResponse| { &m.activation_id }, - |m: &mut GuestActivationStartResponse| { &mut m.activation_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &GuestActivationStartResponse| { &m.state }, - |m: &mut GuestActivationStartResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &GuestActivationStartResponse| { &m.error }, - |m: &mut GuestActivationStartResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "GuestActivationStartResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for GuestActivationStartResponse { - const NAME: &'static str = "GuestActivationStartResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.activation_id = is.read_string()?; - }, - 16 => { - self.state = is.read_enum_or_unknown()?; - }, - 26 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.activation_id.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.activation_id); - } - if self.state != ::protobuf::EnumOrUnknown::new(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.state.value()); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.activation_id.is_empty() { - os.write_string(1, &self.activation_id)?; - } - if self.state != ::protobuf::EnumOrUnknown::new(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> GuestActivationStartResponse { - GuestActivationStartResponse::new() - } - - fn clear(&mut self) { - self.activation_id.clear(); - self.state = ::protobuf::EnumOrUnknown::new(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static GuestActivationStartResponse { - static instance: GuestActivationStartResponse = GuestActivationStartResponse { - activation_id: ::std::string::String::new(), - state: ::protobuf::EnumOrUnknown::from_i32(0), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for GuestActivationStartResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("GuestActivationStartResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for GuestActivationStartResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for GuestActivationStartResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.GuestActivationStatusRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct GuestActivationStatusRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusRequest.activation_id) - pub activation_id: ::std::string::String, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.GuestActivationStatusRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a GuestActivationStatusRequest { - fn default() -> &'a GuestActivationStatusRequest { - ::default_instance() - } -} - -impl GuestActivationStatusRequest { - pub fn new() -> GuestActivationStatusRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &GuestActivationStatusRequest| { &m.metadata }, - |m: &mut GuestActivationStatusRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "activation_id", - |m: &GuestActivationStatusRequest| { &m.activation_id }, - |m: &mut GuestActivationStatusRequest| { &mut m.activation_id }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "GuestActivationStatusRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for GuestActivationStatusRequest { - const NAME: &'static str = "GuestActivationStatusRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.activation_id = is.read_string()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.activation_id.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.activation_id); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.activation_id.is_empty() { - os.write_string(2, &self.activation_id)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> GuestActivationStatusRequest { - GuestActivationStatusRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.activation_id.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static GuestActivationStatusRequest { - static instance: GuestActivationStatusRequest = GuestActivationStatusRequest { - metadata: ::protobuf::MessageField::none(), - activation_id: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for GuestActivationStatusRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("GuestActivationStatusRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for GuestActivationStatusRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for GuestActivationStatusRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.GuestActivationStatusResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct GuestActivationStatusResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusResponse.activation_id) - pub activation_id: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusResponse.exit_code) - pub exit_code: ::std::option::Option, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusResponse.signal) - pub signal: ::std::option::Option, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusResponse.status_code) - pub status_code: ::std::option::Option, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestActivationStatusResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.GuestActivationStatusResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a GuestActivationStatusResponse { - fn default() -> &'a GuestActivationStatusResponse { - ::default_instance() - } -} - -impl GuestActivationStatusResponse { - pub fn new() -> GuestActivationStatusResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(6); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "activation_id", - |m: &GuestActivationStatusResponse| { &m.activation_id }, - |m: &mut GuestActivationStatusResponse| { &mut m.activation_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &GuestActivationStatusResponse| { &m.state }, - |m: &mut GuestActivationStatusResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "exit_code", - |m: &GuestActivationStatusResponse| { &m.exit_code }, - |m: &mut GuestActivationStatusResponse| { &mut m.exit_code }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "signal", - |m: &GuestActivationStatusResponse| { &m.signal }, - |m: &mut GuestActivationStatusResponse| { &mut m.signal }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "status_code", - |m: &GuestActivationStatusResponse| { &m.status_code }, - |m: &mut GuestActivationStatusResponse| { &mut m.status_code }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &GuestActivationStatusResponse| { &m.error }, - |m: &mut GuestActivationStatusResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "GuestActivationStatusResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for GuestActivationStatusResponse { - const NAME: &'static str = "GuestActivationStatusResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.activation_id = is.read_string()?; - }, - 16 => { - self.state = is.read_enum_or_unknown()?; - }, - 24 => { - self.exit_code = ::std::option::Option::Some(is.read_int32()?); - }, - 32 => { - self.signal = ::std::option::Option::Some(is.read_uint32()?); - }, - 40 => { - self.status_code = ::std::option::Option::Some(is.read_int32()?); - }, - 50 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.activation_id.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.activation_id); - } - if self.state != ::protobuf::EnumOrUnknown::new(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.state.value()); - } - if let Some(v) = self.exit_code { - my_size += ::protobuf::rt::int32_size(3, v); - } - if let Some(v) = self.signal { - my_size += ::protobuf::rt::uint32_size(4, v); - } - if let Some(v) = self.status_code { - my_size += ::protobuf::rt::int32_size(5, v); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.activation_id.is_empty() { - os.write_string(1, &self.activation_id)?; - } - if self.state != ::protobuf::EnumOrUnknown::new(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if let Some(v) = self.exit_code { - os.write_int32(3, v)?; - } - if let Some(v) = self.signal { - os.write_uint32(4, v)?; - } - if let Some(v) = self.status_code { - os.write_int32(5, v)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(6, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> GuestActivationStatusResponse { - GuestActivationStatusResponse::new() - } - - fn clear(&mut self) { - self.activation_id.clear(); - self.state = ::protobuf::EnumOrUnknown::new(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED); - self.exit_code = ::std::option::Option::None; - self.signal = ::std::option::Option::None; - self.status_code = ::std::option::Option::None; - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static GuestActivationStatusResponse { - static instance: GuestActivationStatusResponse = GuestActivationStatusResponse { - activation_id: ::std::string::String::new(), - state: ::protobuf::EnumOrUnknown::from_i32(0), - exit_code: ::std::option::Option::None, - signal: ::std::option::Option::None, - status_code: ::std::option::Option::None, - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for GuestActivationStatusResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("GuestActivationStatusResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for GuestActivationStatusResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for GuestActivationStatusResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellAttachRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellAttachRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachRequest.name) - pub name: ::std::option::Option<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachRequest.force) - pub force: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachRequest.initial_terminal_size) - pub initial_terminal_size: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellAttachRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellAttachRequest { - fn default() -> &'a ShellAttachRequest { - ::default_instance() - } -} - -impl ShellAttachRequest { - pub fn new() -> ShellAttachRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &ShellAttachRequest| { &m.metadata }, - |m: &mut ShellAttachRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "name", - |m: &ShellAttachRequest| { &m.name }, - |m: &mut ShellAttachRequest| { &mut m.name }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "force", - |m: &ShellAttachRequest| { &m.force }, - |m: &mut ShellAttachRequest| { &mut m.force }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalSize>( - "initial_terminal_size", - |m: &ShellAttachRequest| { &m.initial_terminal_size }, - |m: &mut ShellAttachRequest| { &mut m.initial_terminal_size }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellAttachRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellAttachRequest { - const NAME: &'static str = "ShellAttachRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.name = ::std::option::Option::Some(is.read_string()?); - }, - 24 => { - self.force = is.read_bool()?; - }, - 34 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.initial_terminal_size)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.name.as_ref() { - my_size += ::protobuf::rt::string_size(2, &v); - } - if self.force != false { - my_size += 1 + 1; - } - if let Some(v) = self.initial_terminal_size.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if let Some(v) = self.name.as_ref() { - os.write_string(2, v)?; - } - if self.force != false { - os.write_bool(3, self.force)?; - } - if let Some(v) = self.initial_terminal_size.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellAttachRequest { - ShellAttachRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.name = ::std::option::Option::None; - self.force = false; - self.initial_terminal_size.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellAttachRequest { - static instance: ShellAttachRequest = ShellAttachRequest { - metadata: ::protobuf::MessageField::none(), - name: ::std::option::Option::None, - force: false, - initial_terminal_size: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellAttachRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellAttachRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellAttachRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellAttachRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellAttachResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellAttachResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.session_id) - pub session_id: ::std::option::Option<::std::string::String>, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.resolved_name) - pub resolved_name: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.force_evicted) - pub force_evicted: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.control_seq) - pub control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.output_cursor) - pub output_cursor: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.effective_limits) - pub effective_limits: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellAttachResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellAttachResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellAttachResponse { - fn default() -> &'a ShellAttachResponse { - ::default_instance() - } -} - -impl ShellAttachResponse { - pub fn new() -> ShellAttachResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(8); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "session_id", - |m: &ShellAttachResponse| { &m.session_id }, - |m: &mut ShellAttachResponse| { &mut m.session_id }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "resolved_name", - |m: &ShellAttachResponse| { &m.resolved_name }, - |m: &mut ShellAttachResponse| { &mut m.resolved_name }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &ShellAttachResponse| { &m.state }, - |m: &mut ShellAttachResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "force_evicted", - |m: &ShellAttachResponse| { &m.force_evicted }, - |m: &mut ShellAttachResponse| { &mut m.force_evicted }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "control_seq", - |m: &ShellAttachResponse| { &m.control_seq }, - |m: &mut ShellAttachResponse| { &mut m.control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "output_cursor", - |m: &ShellAttachResponse| { &m.output_cursor }, - |m: &mut ShellAttachResponse| { &mut m.output_cursor }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestEffectiveLimits>( - "effective_limits", - |m: &ShellAttachResponse| { &m.effective_limits }, - |m: &mut ShellAttachResponse| { &mut m.effective_limits }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ShellAttachResponse| { &m.error }, - |m: &mut ShellAttachResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellAttachResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellAttachResponse { - const NAME: &'static str = "ShellAttachResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.session_id = ::std::option::Option::Some(is.read_string()?); - }, - 18 => { - self.resolved_name = is.read_string()?; - }, - 24 => { - self.state = is.read_enum_or_unknown()?; - }, - 32 => { - self.force_evicted = is.read_bool()?; - }, - 40 => { - self.control_seq = is.read_uint64()?; - }, - 48 => { - self.output_cursor = is.read_uint64()?; - }, - 58 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.effective_limits)?; - }, - 66 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.session_id.as_ref() { - my_size += ::protobuf::rt::string_size(1, &v); - } - if !self.resolved_name.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.resolved_name); - } - if self.state != ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.state.value()); - } - if self.force_evicted != false { - my_size += 1 + 1; - } - if self.control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(5, self.control_seq); - } - if self.output_cursor != 0 { - my_size += ::protobuf::rt::uint64_size(6, self.output_cursor); - } - if let Some(v) = self.effective_limits.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.session_id.as_ref() { - os.write_string(1, v)?; - } - if !self.resolved_name.is_empty() { - os.write_string(2, &self.resolved_name)?; - } - if self.state != ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if self.force_evicted != false { - os.write_bool(4, self.force_evicted)?; - } - if self.control_seq != 0 { - os.write_uint64(5, self.control_seq)?; - } - if self.output_cursor != 0 { - os.write_uint64(6, self.output_cursor)?; - } - if let Some(v) = self.effective_limits.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(7, v, os)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(8, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellAttachResponse { - ShellAttachResponse::new() - } - - fn clear(&mut self) { - self.session_id = ::std::option::Option::None; - self.resolved_name.clear(); - self.state = ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED); - self.force_evicted = false; - self.control_seq = 0; - self.output_cursor = 0; - self.effective_limits.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellAttachResponse { - static instance: ShellAttachResponse = ShellAttachResponse { - session_id: ::std::option::Option::None, - resolved_name: ::std::string::String::new(), - state: ::protobuf::EnumOrUnknown::from_i32(0), - force_evicted: false, - control_seq: 0, - output_cursor: 0, - effective_limits: ::protobuf::MessageField::none(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellAttachResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellAttachResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellAttachResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellAttachResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellListRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellListRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListRequest.metadata) - pub metadata: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellListRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellListRequest { - fn default() -> &'a ShellListRequest { - ::default_instance() - } -} - -impl ShellListRequest { - pub fn new() -> ShellListRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(1); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &ShellListRequest| { &m.metadata }, - |m: &mut ShellListRequest| { &mut m.metadata }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellListRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellListRequest { - const NAME: &'static str = "ShellListRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellListRequest { - ShellListRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellListRequest { - static instance: ShellListRequest = ShellListRequest { - metadata: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellListRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellListRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellListRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellListRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellListResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellListResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListResponse.default_name) - pub default_name: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListResponse.sessions) - pub sessions: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellListResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellListResponse { - fn default() -> &'a ShellListResponse { - ::default_instance() - } -} - -impl ShellListResponse { - pub fn new() -> ShellListResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "default_name", - |m: &ShellListResponse| { &m.default_name }, - |m: &mut ShellListResponse| { &mut m.default_name }, - )); - fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( - "sessions", - |m: &ShellListResponse| { &m.sessions }, - |m: &mut ShellListResponse| { &mut m.sessions }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ShellListResponse| { &m.error }, - |m: &mut ShellListResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellListResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellListResponse { - const NAME: &'static str = "ShellListResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.default_name = is.read_string()?; - }, - 18 => { - self.sessions.push(is.read_message()?); - }, - 26 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.default_name.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.default_name); - } - for value in &self.sessions { - let len = value.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - }; - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.default_name.is_empty() { - os.write_string(1, &self.default_name)?; - } - for v in &self.sessions { - ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; - }; - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellListResponse { - ShellListResponse::new() - } - - fn clear(&mut self) { - self.default_name.clear(); - self.sessions.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellListResponse { - static instance: ShellListResponse = ShellListResponse { - default_name: ::std::string::String::new(), - sessions: ::std::vec::Vec::new(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellListResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellListResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellListResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellListResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellListEntry) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellListEntry { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListEntry.name) - pub name: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListEntry.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListEntry.attached) - pub attached: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellListEntry.is_default) - pub is_default: bool, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellListEntry.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellListEntry { - fn default() -> &'a ShellListEntry { - ::default_instance() - } -} - -impl ShellListEntry { - pub fn new() -> ShellListEntry { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "name", - |m: &ShellListEntry| { &m.name }, - |m: &mut ShellListEntry| { &mut m.name }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &ShellListEntry| { &m.state }, - |m: &mut ShellListEntry| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "attached", - |m: &ShellListEntry| { &m.attached }, - |m: &mut ShellListEntry| { &mut m.attached }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "is_default", - |m: &ShellListEntry| { &m.is_default }, - |m: &mut ShellListEntry| { &mut m.is_default }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellListEntry", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellListEntry { - const NAME: &'static str = "ShellListEntry"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.name = is.read_string()?; - }, - 16 => { - self.state = is.read_enum_or_unknown()?; - }, - 24 => { - self.attached = is.read_bool()?; - }, - 32 => { - self.is_default = is.read_bool()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.name.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.name); - } - if self.state != ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.state.value()); - } - if self.attached != false { - my_size += 1 + 1; - } - if self.is_default != false { - my_size += 1 + 1; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.name.is_empty() { - os.write_string(1, &self.name)?; - } - if self.state != ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if self.attached != false { - os.write_bool(3, self.attached)?; - } - if self.is_default != false { - os.write_bool(4, self.is_default)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellListEntry { - ShellListEntry::new() - } - - fn clear(&mut self) { - self.name.clear(); - self.state = ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED); - self.attached = false; - self.is_default = false; - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellListEntry { - static instance: ShellListEntry = ShellListEntry { - name: ::std::string::String::new(), - state: ::protobuf::EnumOrUnknown::from_i32(0), - attached: false, - is_default: false, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellListEntry { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellListEntry").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellListEntry { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellListEntry { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellDetachRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellDetachRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellDetachRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellDetachRequest.name) - pub name: ::std::option::Option<::std::string::String>, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellDetachRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellDetachRequest { - fn default() -> &'a ShellDetachRequest { - ::default_instance() - } -} - -impl ShellDetachRequest { - pub fn new() -> ShellDetachRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &ShellDetachRequest| { &m.metadata }, - |m: &mut ShellDetachRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "name", - |m: &ShellDetachRequest| { &m.name }, - |m: &mut ShellDetachRequest| { &mut m.name }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellDetachRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellDetachRequest { - const NAME: &'static str = "ShellDetachRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.name = ::std::option::Option::Some(is.read_string()?); - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.name.as_ref() { - my_size += ::protobuf::rt::string_size(2, &v); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if let Some(v) = self.name.as_ref() { - os.write_string(2, v)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellDetachRequest { - ShellDetachRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.name = ::std::option::Option::None; - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellDetachRequest { - static instance: ShellDetachRequest = ShellDetachRequest { - metadata: ::protobuf::MessageField::none(), - name: ::std::option::Option::None, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellDetachRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellDetachRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellDetachRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellDetachRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellKillRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellKillRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellKillRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellKillRequest.name) - pub name: ::std::string::String, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellKillRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellKillRequest { - fn default() -> &'a ShellKillRequest { - ::default_instance() - } -} - -impl ShellKillRequest { - pub fn new() -> ShellKillRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &ShellKillRequest| { &m.metadata }, - |m: &mut ShellKillRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "name", - |m: &ShellKillRequest| { &m.name }, - |m: &mut ShellKillRequest| { &mut m.name }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellKillRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellKillRequest { - const NAME: &'static str = "ShellKillRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 18 => { - self.name = is.read_string()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if !self.name.is_empty() { - my_size += ::protobuf::rt::string_size(2, &self.name); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if !self.name.is_empty() { - os.write_string(2, &self.name)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellKillRequest { - ShellKillRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.name.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellKillRequest { - static instance: ShellKillRequest = ShellKillRequest { - metadata: ::protobuf::MessageField::none(), - name: ::std::string::String::new(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellKillRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellKillRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellKillRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellKillRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellCloseAttachRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellCloseAttachRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellCloseAttachRequest.metadata) - pub metadata: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellCloseAttachRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellCloseAttachRequest { - fn default() -> &'a ShellCloseAttachRequest { - ::default_instance() - } -} - -impl ShellCloseAttachRequest { - pub fn new() -> ShellCloseAttachRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(1); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalRequestMetadata>( - "metadata", - |m: &ShellCloseAttachRequest| { &m.metadata }, - |m: &mut ShellCloseAttachRequest| { &mut m.metadata }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellCloseAttachRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellCloseAttachRequest { - const NAME: &'static str = "ShellCloseAttachRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellCloseAttachRequest { - ShellCloseAttachRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellCloseAttachRequest { - static instance: ShellCloseAttachRequest = ShellCloseAttachRequest { - metadata: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellCloseAttachRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellCloseAttachRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellCloseAttachRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellCloseAttachRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellDetachResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellDetachResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellDetachResponse.resolved_name) - pub resolved_name: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellDetachResponse.detached) - pub detached: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellDetachResponse.cause) - pub cause: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellDetachResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellDetachResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellDetachResponse { - fn default() -> &'a ShellDetachResponse { - ::default_instance() - } -} - -impl ShellDetachResponse { - pub fn new() -> ShellDetachResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "resolved_name", - |m: &ShellDetachResponse| { &m.resolved_name }, - |m: &mut ShellDetachResponse| { &mut m.resolved_name }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "detached", - |m: &ShellDetachResponse| { &m.detached }, - |m: &mut ShellDetachResponse| { &mut m.detached }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "cause", - |m: &ShellDetachResponse| { &m.cause }, - |m: &mut ShellDetachResponse| { &mut m.cause }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ShellDetachResponse| { &m.error }, - |m: &mut ShellDetachResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellDetachResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellDetachResponse { - const NAME: &'static str = "ShellDetachResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.resolved_name = is.read_string()?; - }, - 16 => { - self.detached = is.read_bool()?; - }, - 24 => { - self.cause = is.read_enum_or_unknown()?; - }, - 34 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.resolved_name.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.resolved_name); - } - if self.detached != false { - my_size += 1 + 1; - } - if self.cause != ::protobuf::EnumOrUnknown::new(ShellCloseCause::SHELL_CLOSE_CAUSE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.cause.value()); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.resolved_name.is_empty() { - os.write_string(1, &self.resolved_name)?; - } - if self.detached != false { - os.write_bool(2, self.detached)?; - } - if self.cause != ::protobuf::EnumOrUnknown::new(ShellCloseCause::SHELL_CLOSE_CAUSE_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.cause))?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellDetachResponse { - ShellDetachResponse::new() - } - - fn clear(&mut self) { - self.resolved_name.clear(); - self.detached = false; - self.cause = ::protobuf::EnumOrUnknown::new(ShellCloseCause::SHELL_CLOSE_CAUSE_UNSPECIFIED); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellDetachResponse { - static instance: ShellDetachResponse = ShellDetachResponse { - resolved_name: ::std::string::String::new(), - detached: false, - cause: ::protobuf::EnumOrUnknown::from_i32(0), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellDetachResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellDetachResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellDetachResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellDetachResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.ShellKillResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct ShellKillResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.ShellKillResponse.name) - pub name: ::std::string::String, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellKillResponse.killed) - pub killed: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellKillResponse.state) - pub state: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.ShellKillResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.ShellKillResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a ShellKillResponse { - fn default() -> &'a ShellKillResponse { - ::default_instance() - } -} - -impl ShellKillResponse { - pub fn new() -> ShellKillResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "name", - |m: &ShellKillResponse| { &m.name }, - |m: &mut ShellKillResponse| { &mut m.name }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "killed", - |m: &ShellKillResponse| { &m.killed }, - |m: &mut ShellKillResponse| { &mut m.killed }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "state", - |m: &ShellKillResponse| { &m.state }, - |m: &mut ShellKillResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &ShellKillResponse| { &m.error }, - |m: &mut ShellKillResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "ShellKillResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for ShellKillResponse { - const NAME: &'static str = "ShellKillResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - self.name = is.read_string()?; - }, - 16 => { - self.killed = is.read_bool()?; - }, - 24 => { - self.state = is.read_enum_or_unknown()?; - }, - 34 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if !self.name.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.name); - } - if self.killed != false { - my_size += 1 + 1; - } - if self.state != ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.state.value()); - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if !self.name.is_empty() { - os.write_string(1, &self.name)?; - } - if self.killed != false { - os.write_bool(2, self.killed)?; - } - if self.state != ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.state))?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> ShellKillResponse { - ShellKillResponse::new() - } - - fn clear(&mut self) { - self.name.clear(); - self.killed = false; - self.state = ::protobuf::EnumOrUnknown::new(ShellState::SHELL_STATE_UNSPECIFIED); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static ShellKillResponse { - static instance: ShellKillResponse = ShellKillResponse { - name: ::std::string::String::new(), - killed: false, - state: ::protobuf::EnumOrUnknown::from_i32(0), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for ShellKillResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("ShellKillResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for ShellKillResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for ShellKillResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TerminalWriteStdinRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TerminalWriteStdinRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalWriteStdinRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalWriteStdinRequest.offset) - pub offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalWriteStdinRequest.data) - pub data: ::std::vec::Vec, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalWriteStdinRequest.close_after) - pub close_after: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalWriteStdinRequest.client_deadline_ms) - pub client_deadline_ms: ::std::option::Option, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TerminalWriteStdinRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TerminalWriteStdinRequest { - fn default() -> &'a TerminalWriteStdinRequest { - ::default_instance() - } -} - -impl TerminalWriteStdinRequest { - pub fn new() -> TerminalWriteStdinRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(5); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalRequestMetadata>( - "metadata", - |m: &TerminalWriteStdinRequest| { &m.metadata }, - |m: &mut TerminalWriteStdinRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &TerminalWriteStdinRequest| { &m.offset }, - |m: &mut TerminalWriteStdinRequest| { &mut m.offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "data", - |m: &TerminalWriteStdinRequest| { &m.data }, - |m: &mut TerminalWriteStdinRequest| { &mut m.data }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "close_after", - |m: &TerminalWriteStdinRequest| { &m.close_after }, - |m: &mut TerminalWriteStdinRequest| { &mut m.close_after }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "client_deadline_ms", - |m: &TerminalWriteStdinRequest| { &m.client_deadline_ms }, - |m: &mut TerminalWriteStdinRequest| { &mut m.client_deadline_ms }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TerminalWriteStdinRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TerminalWriteStdinRequest { - const NAME: &'static str = "TerminalWriteStdinRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.offset = is.read_uint64()?; - }, - 26 => { - self.data = is.read_bytes()?; - }, - 32 => { - self.close_after = is.read_bool()?; - }, - 40 => { - self.client_deadline_ms = ::std::option::Option::Some(is.read_uint64()?); - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.offset); - } - if !self.data.is_empty() { - my_size += ::protobuf::rt::bytes_size(3, &self.data); - } - if self.close_after != false { - my_size += 1 + 1; - } - if let Some(v) = self.client_deadline_ms { - my_size += ::protobuf::rt::uint64_size(5, v); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.offset != 0 { - os.write_uint64(2, self.offset)?; - } - if !self.data.is_empty() { - os.write_bytes(3, &self.data)?; - } - if self.close_after != false { - os.write_bool(4, self.close_after)?; - } - if let Some(v) = self.client_deadline_ms { - os.write_uint64(5, v)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TerminalWriteStdinRequest { - TerminalWriteStdinRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.offset = 0; - self.data.clear(); - self.close_after = false; - self.client_deadline_ms = ::std::option::Option::None; - self.special_fields.clear(); - } - - fn default_instance() -> &'static TerminalWriteStdinRequest { - static instance: TerminalWriteStdinRequest = TerminalWriteStdinRequest { - metadata: ::protobuf::MessageField::none(), - offset: 0, - data: ::std::vec::Vec::new(), - close_after: false, - client_deadline_ms: ::std::option::Option::None, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TerminalWriteStdinRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TerminalWriteStdinRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TerminalWriteStdinRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TerminalWriteStdinRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TerminalReadOutputRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TerminalReadOutputRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalReadOutputRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalReadOutputRequest.stream) - pub stream: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalReadOutputRequest.offset) - pub offset: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalReadOutputRequest.max_len) - pub max_len: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalReadOutputRequest.wait) - pub wait: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalReadOutputRequest.timeout_ms) - pub timeout_ms: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TerminalReadOutputRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TerminalReadOutputRequest { - fn default() -> &'a TerminalReadOutputRequest { - ::default_instance() - } -} - -impl TerminalReadOutputRequest { - pub fn new() -> TerminalReadOutputRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(6); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalRequestMetadata>( - "metadata", - |m: &TerminalReadOutputRequest| { &m.metadata }, - |m: &mut TerminalReadOutputRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "stream", - |m: &TerminalReadOutputRequest| { &m.stream }, - |m: &mut TerminalReadOutputRequest| { &mut m.stream }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &TerminalReadOutputRequest| { &m.offset }, - |m: &mut TerminalReadOutputRequest| { &mut m.offset }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "max_len", - |m: &TerminalReadOutputRequest| { &m.max_len }, - |m: &mut TerminalReadOutputRequest| { &mut m.max_len }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "wait", - |m: &TerminalReadOutputRequest| { &m.wait }, - |m: &mut TerminalReadOutputRequest| { &mut m.wait }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "timeout_ms", - |m: &TerminalReadOutputRequest| { &m.timeout_ms }, - |m: &mut TerminalReadOutputRequest| { &mut m.timeout_ms }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TerminalReadOutputRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TerminalReadOutputRequest { - const NAME: &'static str = "TerminalReadOutputRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.stream = is.read_enum_or_unknown()?; - }, - 24 => { - self.offset = is.read_uint64()?; - }, - 32 => { - self.max_len = is.read_uint64()?; - }, - 40 => { - self.wait = is.read_bool()?; - }, - 48 => { - self.timeout_ms = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.stream.value()); - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(3, self.offset); - } - if self.max_len != 0 { - my_size += ::protobuf::rt::uint64_size(4, self.max_len); - } - if self.wait != false { - my_size += 1 + 1; - } - if self.timeout_ms != 0 { - my_size += ::protobuf::rt::uint64_size(6, self.timeout_ms); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.stream != ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.stream))?; - } - if self.offset != 0 { - os.write_uint64(3, self.offset)?; - } - if self.max_len != 0 { - os.write_uint64(4, self.max_len)?; - } - if self.wait != false { - os.write_bool(5, self.wait)?; - } - if self.timeout_ms != 0 { - os.write_uint64(6, self.timeout_ms)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TerminalReadOutputRequest { - TerminalReadOutputRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.stream = ::protobuf::EnumOrUnknown::new(OutputStream::OUTPUT_STREAM_UNSPECIFIED); - self.offset = 0; - self.max_len = 0; - self.wait = false; - self.timeout_ms = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static TerminalReadOutputRequest { - static instance: TerminalReadOutputRequest = TerminalReadOutputRequest { - metadata: ::protobuf::MessageField::none(), - stream: ::protobuf::EnumOrUnknown::from_i32(0), - offset: 0, - max_len: 0, - wait: false, - timeout_ms: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TerminalReadOutputRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TerminalReadOutputRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TerminalReadOutputRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TerminalReadOutputRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TerminalCloseStdinRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TerminalCloseStdinRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalCloseStdinRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalCloseStdinRequest.offset) - pub offset: u64, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TerminalCloseStdinRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TerminalCloseStdinRequest { - fn default() -> &'a TerminalCloseStdinRequest { - ::default_instance() - } -} - -impl TerminalCloseStdinRequest { - pub fn new() -> TerminalCloseStdinRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalRequestMetadata>( - "metadata", - |m: &TerminalCloseStdinRequest| { &m.metadata }, - |m: &mut TerminalCloseStdinRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "offset", - |m: &TerminalCloseStdinRequest| { &m.offset }, - |m: &mut TerminalCloseStdinRequest| { &mut m.offset }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TerminalCloseStdinRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TerminalCloseStdinRequest { - const NAME: &'static str = "TerminalCloseStdinRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.offset = is.read_uint64()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.offset != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.offset); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.offset != 0 { - os.write_uint64(2, self.offset)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TerminalCloseStdinRequest { - TerminalCloseStdinRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.offset = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static TerminalCloseStdinRequest { - static instance: TerminalCloseStdinRequest = TerminalCloseStdinRequest { - metadata: ::protobuf::MessageField::none(), - offset: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TerminalCloseStdinRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TerminalCloseStdinRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TerminalCloseStdinRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TerminalCloseStdinRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TerminalTtyWinResizeRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TerminalTtyWinResizeRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalTtyWinResizeRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalTtyWinResizeRequest.control_seq) - pub control_seq: u64, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalTtyWinResizeRequest.rows) - pub rows: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.TerminalTtyWinResizeRequest.cols) - pub cols: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TerminalTtyWinResizeRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TerminalTtyWinResizeRequest { - fn default() -> &'a TerminalTtyWinResizeRequest { - ::default_instance() - } -} - -impl TerminalTtyWinResizeRequest { - pub fn new() -> TerminalTtyWinResizeRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TerminalRequestMetadata>( - "metadata", - |m: &TerminalTtyWinResizeRequest| { &m.metadata }, - |m: &mut TerminalTtyWinResizeRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "control_seq", - |m: &TerminalTtyWinResizeRequest| { &m.control_seq }, - |m: &mut TerminalTtyWinResizeRequest| { &mut m.control_seq }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "rows", - |m: &TerminalTtyWinResizeRequest| { &m.rows }, - |m: &mut TerminalTtyWinResizeRequest| { &mut m.rows }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "cols", - |m: &TerminalTtyWinResizeRequest| { &m.cols }, - |m: &mut TerminalTtyWinResizeRequest| { &mut m.cols }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TerminalTtyWinResizeRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TerminalTtyWinResizeRequest { - const NAME: &'static str = "TerminalTtyWinResizeRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.control_seq = is.read_uint64()?; - }, - 24 => { - self.rows = is.read_uint32()?; - }, - 32 => { - self.cols = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.control_seq != 0 { - my_size += ::protobuf::rt::uint64_size(2, self.control_seq); - } - if self.rows != 0 { - my_size += ::protobuf::rt::uint32_size(3, self.rows); - } - if self.cols != 0 { - my_size += ::protobuf::rt::uint32_size(4, self.cols); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.control_seq != 0 { - os.write_uint64(2, self.control_seq)?; - } - if self.rows != 0 { - os.write_uint32(3, self.rows)?; - } - if self.cols != 0 { - os.write_uint32(4, self.cols)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TerminalTtyWinResizeRequest { - TerminalTtyWinResizeRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.control_seq = 0; - self.rows = 0; - self.cols = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static TerminalTtyWinResizeRequest { - static instance: TerminalTtyWinResizeRequest = TerminalTtyWinResizeRequest { - metadata: ::protobuf::MessageField::none(), - control_seq: 0, - rows: 0, - cols: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TerminalTtyWinResizeRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TerminalTtyWinResizeRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TerminalTtyWinResizeRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TerminalTtyWinResizeRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.TerminalStatus) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct TerminalStatus { - // message oneof groups - pub outcome: ::std::option::Option, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.TerminalStatus.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a TerminalStatus { - fn default() -> &'a TerminalStatus { - ::default_instance() - } -} - -impl TerminalStatus { - pub fn new() -> TerminalStatus { - ::std::default::Default::default() - } - - // int32 exit_code = 1; - - pub fn exit_code(&self) -> i32 { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::ExitCode(v)) => v, - _ => 0, - } - } - - pub fn clear_exit_code(&mut self) { - self.outcome = ::std::option::Option::None; - } - - pub fn has_exit_code(&self) -> bool { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::ExitCode(..)) => true, - _ => false, - } - } - - // Param is passed by value, moved - pub fn set_exit_code(&mut self, v: i32) { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::ExitCode(v)) - } - - // uint32 signal = 2; - - pub fn signal(&self) -> u32 { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::Signal(v)) => v, - _ => 0, - } - } - - pub fn clear_signal(&mut self) { - self.outcome = ::std::option::Option::None; - } - - pub fn has_signal(&self) -> bool { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::Signal(..)) => true, - _ => false, - } - } - - // Param is passed by value, moved - pub fn set_signal(&mut self, v: u32) { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::Signal(v)) - } - - // int32 status_code = 3; - - pub fn status_code(&self) -> i32 { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::StatusCode(v)) => v, - _ => 0, - } - } - - pub fn clear_status_code(&mut self) { - self.outcome = ::std::option::Option::None; - } - - pub fn has_status_code(&self) -> bool { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::StatusCode(..)) => true, - _ => false, - } - } - - // Param is passed by value, moved - pub fn set_status_code(&mut self, v: i32) { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::StatusCode(v)) - } - - // .d2b.guest.v1.GuestControlErrorKind error = 4; - - pub fn error(&self) -> GuestControlErrorKind { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::Error(v)) => ::protobuf::EnumOrUnknown::enum_value_or_default(&v), - _ => GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED, - } - } - - pub fn clear_error(&mut self) { - self.outcome = ::std::option::Option::None; - } - - pub fn has_error(&self) -> bool { - match self.outcome { - ::std::option::Option::Some(terminal_status::Outcome::Error(..)) => true, - _ => false, - } - } - - // Param is passed by value, moved - pub fn set_error(&mut self, v: GuestControlErrorKind) { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::Error(::protobuf::EnumOrUnknown::new(v))) - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(4); - let mut oneofs = ::std::vec::Vec::with_capacity(1); - fields.push(::protobuf::reflect::rt::v2::make_oneof_copy_has_get_set_simpler_accessors::<_, _>( - "exit_code", - TerminalStatus::has_exit_code, - TerminalStatus::exit_code, - TerminalStatus::set_exit_code, - )); - fields.push(::protobuf::reflect::rt::v2::make_oneof_copy_has_get_set_simpler_accessors::<_, _>( - "signal", - TerminalStatus::has_signal, - TerminalStatus::signal, - TerminalStatus::set_signal, - )); - fields.push(::protobuf::reflect::rt::v2::make_oneof_copy_has_get_set_simpler_accessors::<_, _>( - "status_code", - TerminalStatus::has_status_code, - TerminalStatus::status_code, - TerminalStatus::set_status_code, - )); - fields.push(::protobuf::reflect::rt::v2::make_oneof_enum_accessors::<_, _>( - "error", - |message: &TerminalStatus| match &message.outcome { - ::std::option::Option::Some(terminal_status::Outcome::Error(e)) => ::std::option::Option::Some(*e), - _ => ::std::option::Option::None, - }, - |message: &mut TerminalStatus, e: ::protobuf::EnumOrUnknown| { - message.outcome = ::std::option::Option::Some(terminal_status::Outcome::Error(e)); - }, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED, - )); - oneofs.push(terminal_status::Outcome::generated_oneof_descriptor_data()); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "TerminalStatus", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for TerminalStatus { - const NAME: &'static str = "TerminalStatus"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::ExitCode(is.read_int32()?)); - }, - 16 => { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::Signal(is.read_uint32()?)); - }, - 24 => { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::StatusCode(is.read_int32()?)); - }, - 32 => { - self.outcome = ::std::option::Option::Some(terminal_status::Outcome::Error(is.read_enum_or_unknown()?)); - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let ::std::option::Option::Some(ref v) = self.outcome { - match v { - &terminal_status::Outcome::ExitCode(v) => { - my_size += ::protobuf::rt::int32_size(1, v); - }, - &terminal_status::Outcome::Signal(v) => { - my_size += ::protobuf::rt::uint32_size(2, v); - }, - &terminal_status::Outcome::StatusCode(v) => { - my_size += ::protobuf::rt::int32_size(3, v); - }, - &terminal_status::Outcome::Error(v) => { - my_size += ::protobuf::rt::int32_size(4, v.value()); - }, - }; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let ::std::option::Option::Some(ref v) = self.outcome { - match v { - &terminal_status::Outcome::ExitCode(v) => { - os.write_int32(1, v)?; - }, - &terminal_status::Outcome::Signal(v) => { - os.write_uint32(2, v)?; - }, - &terminal_status::Outcome::StatusCode(v) => { - os.write_int32(3, v)?; - }, - &terminal_status::Outcome::Error(v) => { - os.write_enum(4, ::protobuf::EnumOrUnknown::value(&v))?; - }, - }; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> TerminalStatus { - TerminalStatus::new() - } - - fn clear(&mut self) { - self.outcome = ::std::option::Option::None; - self.outcome = ::std::option::Option::None; - self.outcome = ::std::option::Option::None; - self.outcome = ::std::option::Option::None; - self.special_fields.clear(); - } - - fn default_instance() -> &'static TerminalStatus { - static instance: TerminalStatus = TerminalStatus { - outcome: ::std::option::Option::None, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for TerminalStatus { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("TerminalStatus").unwrap()).clone() - } -} - -impl ::std::fmt::Display for TerminalStatus { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for TerminalStatus { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -/// Nested message and enums of message `TerminalStatus` -pub mod terminal_status { - - #[derive(Clone,PartialEq,Debug)] - #[non_exhaustive] - // @@protoc_insertion_point(oneof:d2b.guest.v1.TerminalStatus.outcome) - pub enum Outcome { - // @@protoc_insertion_point(oneof_field:d2b.guest.v1.TerminalStatus.exit_code) - ExitCode(i32), - // @@protoc_insertion_point(oneof_field:d2b.guest.v1.TerminalStatus.signal) - Signal(u32), - // @@protoc_insertion_point(oneof_field:d2b.guest.v1.TerminalStatus.status_code) - StatusCode(i32), - // @@protoc_insertion_point(oneof_field:d2b.guest.v1.TerminalStatus.error) - Error(::protobuf::EnumOrUnknown), - } - - impl ::protobuf::Oneof for Outcome { - } - - impl ::protobuf::OneofFull for Outcome { - fn descriptor() -> ::protobuf::reflect::OneofDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::OneofDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| ::descriptor().oneof_by_name("outcome").unwrap()).clone() - } - } - - impl Outcome { - pub(in super) fn generated_oneof_descriptor_data() -> ::protobuf::reflect::GeneratedOneofDescriptorData { - ::protobuf::reflect::GeneratedOneofDescriptorData::new::("outcome") - } - } -} - -// @@protoc_insertion_point(message:d2b.guest.v1.GuestControlError) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct GuestControlError { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.GuestControlError.kind) - pub kind: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestControlError.remediation) - pub remediation: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestControlError.retry_after_ms) - pub retry_after_ms: ::std::option::Option, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.GuestControlError.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a GuestControlError { - fn default() -> &'a GuestControlError { - ::default_instance() - } -} - -impl GuestControlError { - pub fn new() -> GuestControlError { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "kind", - |m: &GuestControlError| { &m.kind }, - |m: &mut GuestControlError| { &mut m.kind }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "remediation", - |m: &GuestControlError| { &m.remediation }, - |m: &mut GuestControlError| { &mut m.remediation }, - )); - fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "retry_after_ms", - |m: &GuestControlError| { &m.retry_after_ms }, - |m: &mut GuestControlError| { &mut m.retry_after_ms }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "GuestControlError", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for GuestControlError { - const NAME: &'static str = "GuestControlError"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.kind = is.read_enum_or_unknown()?; - }, - 16 => { - self.remediation = is.read_enum_or_unknown()?; - }, - 24 => { - self.retry_after_ms = ::std::option::Option::Some(is.read_uint64()?); - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.kind != ::protobuf::EnumOrUnknown::new(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(1, self.kind.value()); - } - if self.remediation != ::protobuf::EnumOrUnknown::new(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.remediation.value()); - } - if let Some(v) = self.retry_after_ms { - my_size += ::protobuf::rt::uint64_size(3, v); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.kind != ::protobuf::EnumOrUnknown::new(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED) { - os.write_enum(1, ::protobuf::EnumOrUnknown::value(&self.kind))?; - } - if self.remediation != ::protobuf::EnumOrUnknown::new(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.remediation))?; - } - if let Some(v) = self.retry_after_ms { - os.write_uint64(3, v)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> GuestControlError { - GuestControlError::new() - } - - fn clear(&mut self) { - self.kind = ::protobuf::EnumOrUnknown::new(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED); - self.remediation = ::protobuf::EnumOrUnknown::new(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED); - self.retry_after_ms = ::std::option::Option::None; - self.special_fields.clear(); - } - - fn default_instance() -> &'static GuestControlError { - static instance: GuestControlError = GuestControlError { - kind: ::protobuf::EnumOrUnknown::from_i32(0), - remediation: ::protobuf::EnumOrUnknown::from_i32(0), - retry_after_ms: ::std::option::Option::None, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for GuestControlError { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("GuestControlError").unwrap()).clone() - } -} - -impl ::std::fmt::Display for GuestControlError { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for GuestControlError { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.GuestAudioChannelState) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct GuestAudioChannelState { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.GuestAudioChannelState.muted) - pub muted: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestAudioChannelState.level) - pub level: u32, - // @@protoc_insertion_point(field:d2b.guest.v1.GuestAudioChannelState.level_known) - pub level_known: bool, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.GuestAudioChannelState.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a GuestAudioChannelState { - fn default() -> &'a GuestAudioChannelState { - ::default_instance() - } -} - -impl GuestAudioChannelState { - pub fn new() -> GuestAudioChannelState { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "muted", - |m: &GuestAudioChannelState| { &m.muted }, - |m: &mut GuestAudioChannelState| { &mut m.muted }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "level", - |m: &GuestAudioChannelState| { &m.level }, - |m: &mut GuestAudioChannelState| { &mut m.level }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "level_known", - |m: &GuestAudioChannelState| { &m.level_known }, - |m: &mut GuestAudioChannelState| { &mut m.level_known }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "GuestAudioChannelState", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for GuestAudioChannelState { - const NAME: &'static str = "GuestAudioChannelState"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 8 => { - self.muted = is.read_bool()?; - }, - 16 => { - self.level = is.read_uint32()?; - }, - 24 => { - self.level_known = is.read_bool()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if self.muted != false { - my_size += 1 + 1; - } - if self.level != 0 { - my_size += ::protobuf::rt::uint32_size(2, self.level); - } - if self.level_known != false { - my_size += 1 + 1; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if self.muted != false { - os.write_bool(1, self.muted)?; - } - if self.level != 0 { - os.write_uint32(2, self.level)?; - } - if self.level_known != false { - os.write_bool(3, self.level_known)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> GuestAudioChannelState { - GuestAudioChannelState::new() - } - - fn clear(&mut self) { - self.muted = false; - self.level = 0; - self.level_known = false; - self.special_fields.clear(); - } - - fn default_instance() -> &'static GuestAudioChannelState { - static instance: GuestAudioChannelState = GuestAudioChannelState { - muted: false, - level: 0, - level_known: false, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for GuestAudioChannelState { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("GuestAudioChannelState").unwrap()).clone() - } -} - -impl ::std::fmt::Display for GuestAudioChannelState { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for GuestAudioChannelState { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.AudioStatusRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct AudioStatusRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.AudioStatusRequest.metadata) - pub metadata: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.AudioStatusRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a AudioStatusRequest { - fn default() -> &'a AudioStatusRequest { - ::default_instance() - } -} - -impl AudioStatusRequest { - pub fn new() -> AudioStatusRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(1); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &AudioStatusRequest| { &m.metadata }, - |m: &mut AudioStatusRequest| { &mut m.metadata }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "AudioStatusRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for AudioStatusRequest { - const NAME: &'static str = "AudioStatusRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> AudioStatusRequest { - AudioStatusRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static AudioStatusRequest { - static instance: AudioStatusRequest = AudioStatusRequest { - metadata: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for AudioStatusRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("AudioStatusRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for AudioStatusRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for AudioStatusRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.AudioStatusResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct AudioStatusResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.AudioStatusResponse.microphone) - pub microphone: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.AudioStatusResponse.speaker) - pub speaker: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.AudioStatusResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.AudioStatusResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a AudioStatusResponse { - fn default() -> &'a AudioStatusResponse { - ::default_instance() - } -} - -impl AudioStatusResponse { - pub fn new() -> AudioStatusResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestAudioChannelState>( - "microphone", - |m: &AudioStatusResponse| { &m.microphone }, - |m: &mut AudioStatusResponse| { &mut m.microphone }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestAudioChannelState>( - "speaker", - |m: &AudioStatusResponse| { &m.speaker }, - |m: &mut AudioStatusResponse| { &mut m.speaker }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &AudioStatusResponse| { &m.error }, - |m: &mut AudioStatusResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "AudioStatusResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for AudioStatusResponse { - const NAME: &'static str = "AudioStatusResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.microphone)?; - }, - 18 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.speaker)?; - }, - 26 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.microphone.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.speaker.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.microphone.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if let Some(v) = self.speaker.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> AudioStatusResponse { - AudioStatusResponse::new() - } - - fn clear(&mut self) { - self.microphone.clear(); - self.speaker.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static AudioStatusResponse { - static instance: AudioStatusResponse = AudioStatusResponse { - microphone: ::protobuf::MessageField::none(), - speaker: ::protobuf::MessageField::none(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for AudioStatusResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("AudioStatusResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for AudioStatusResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for AudioStatusResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.AudioSetRequest) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct AudioSetRequest { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.AudioSetRequest.metadata) - pub metadata: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.AudioSetRequest.channel) - pub channel: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.AudioSetRequest.kind) - pub kind: ::protobuf::EnumOrUnknown, - // @@protoc_insertion_point(field:d2b.guest.v1.AudioSetRequest.grant_on) - pub grant_on: bool, - // @@protoc_insertion_point(field:d2b.guest.v1.AudioSetRequest.level) - pub level: u32, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.AudioSetRequest.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a AudioSetRequest { - fn default() -> &'a AudioSetRequest { - ::default_instance() - } -} - -impl AudioSetRequest { - pub fn new() -> AudioSetRequest { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(5); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMetadata>( - "metadata", - |m: &AudioSetRequest| { &m.metadata }, - |m: &mut AudioSetRequest| { &mut m.metadata }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "channel", - |m: &AudioSetRequest| { &m.channel }, - |m: &mut AudioSetRequest| { &mut m.channel }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "kind", - |m: &AudioSetRequest| { &m.kind }, - |m: &mut AudioSetRequest| { &mut m.kind }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "grant_on", - |m: &AudioSetRequest| { &m.grant_on }, - |m: &mut AudioSetRequest| { &mut m.grant_on }, - )); - fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( - "level", - |m: &AudioSetRequest| { &m.level }, - |m: &mut AudioSetRequest| { &mut m.level }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "AudioSetRequest", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for AudioSetRequest { - const NAME: &'static str = "AudioSetRequest"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.metadata)?; - }, - 16 => { - self.channel = is.read_enum_or_unknown()?; - }, - 24 => { - self.kind = is.read_enum_or_unknown()?; - }, - 32 => { - self.grant_on = is.read_bool()?; - }, - 40 => { - self.level = is.read_uint32()?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.metadata.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if self.channel != ::protobuf::EnumOrUnknown::new(AudioChannel::AUDIO_CHANNEL_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(2, self.channel.value()); - } - if self.kind != ::protobuf::EnumOrUnknown::new(AudioSetKind::AUDIO_SET_KIND_UNSPECIFIED) { - my_size += ::protobuf::rt::int32_size(3, self.kind.value()); - } - if self.grant_on != false { - my_size += 1 + 1; - } - if self.level != 0 { - my_size += ::protobuf::rt::uint32_size(5, self.level); - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.metadata.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if self.channel != ::protobuf::EnumOrUnknown::new(AudioChannel::AUDIO_CHANNEL_UNSPECIFIED) { - os.write_enum(2, ::protobuf::EnumOrUnknown::value(&self.channel))?; - } - if self.kind != ::protobuf::EnumOrUnknown::new(AudioSetKind::AUDIO_SET_KIND_UNSPECIFIED) { - os.write_enum(3, ::protobuf::EnumOrUnknown::value(&self.kind))?; - } - if self.grant_on != false { - os.write_bool(4, self.grant_on)?; - } - if self.level != 0 { - os.write_uint32(5, self.level)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> AudioSetRequest { - AudioSetRequest::new() - } - - fn clear(&mut self) { - self.metadata.clear(); - self.channel = ::protobuf::EnumOrUnknown::new(AudioChannel::AUDIO_CHANNEL_UNSPECIFIED); - self.kind = ::protobuf::EnumOrUnknown::new(AudioSetKind::AUDIO_SET_KIND_UNSPECIFIED); - self.grant_on = false; - self.level = 0; - self.special_fields.clear(); - } - - fn default_instance() -> &'static AudioSetRequest { - static instance: AudioSetRequest = AudioSetRequest { - metadata: ::protobuf::MessageField::none(), - channel: ::protobuf::EnumOrUnknown::from_i32(0), - kind: ::protobuf::EnumOrUnknown::from_i32(0), - grant_on: false, - level: 0, - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for AudioSetRequest { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("AudioSetRequest").unwrap()).clone() - } -} - -impl ::std::fmt::Display for AudioSetRequest { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for AudioSetRequest { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -// @@protoc_insertion_point(message:d2b.guest.v1.AudioSetResponse) -#[derive(PartialEq,Clone,Default,Debug)] -pub struct AudioSetResponse { - // message fields - // @@protoc_insertion_point(field:d2b.guest.v1.AudioSetResponse.state) - pub state: ::protobuf::MessageField, - // @@protoc_insertion_point(field:d2b.guest.v1.AudioSetResponse.error) - pub error: ::protobuf::MessageField, - // special fields - // @@protoc_insertion_point(special_field:d2b.guest.v1.AudioSetResponse.special_fields) - pub special_fields: ::protobuf::SpecialFields, -} - -impl<'a> ::std::default::Default for &'a AudioSetResponse { - fn default() -> &'a AudioSetResponse { - ::default_instance() - } -} - -impl AudioSetResponse { - pub fn new() -> AudioSetResponse { - ::std::default::Default::default() - } - - fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); - let mut oneofs = ::std::vec::Vec::with_capacity(0); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestAudioChannelState>( - "state", - |m: &AudioSetResponse| { &m.state }, - |m: &mut AudioSetResponse| { &mut m.state }, - )); - fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, GuestControlError>( - "error", - |m: &AudioSetResponse| { &m.error }, - |m: &mut AudioSetResponse| { &mut m.error }, - )); - ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( - "AudioSetResponse", - fields, - oneofs, - ) - } -} - -impl ::protobuf::Message for AudioSetResponse { - const NAME: &'static str = "AudioSetResponse"; - - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { - while let Some(tag) = is.read_raw_tag_or_eof()? { - match tag { - 10 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.state)?; - }, - 18 => { - ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; - }, - tag => { - ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u64 { - let mut my_size = 0; - if let Some(v) = self.state.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - if let Some(v) = self.error.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; - } - my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); - self.special_fields.cached_size().set(my_size as u32); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { - if let Some(v) = self.state.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; - } - if let Some(v) = self.error.as_ref() { - ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; - } - os.write_unknown_fields(self.special_fields.unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn special_fields(&self) -> &::protobuf::SpecialFields { - &self.special_fields - } - - fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { - &mut self.special_fields - } - - fn new() -> AudioSetResponse { - AudioSetResponse::new() - } - - fn clear(&mut self) { - self.state.clear(); - self.error.clear(); - self.special_fields.clear(); - } - - fn default_instance() -> &'static AudioSetResponse { - static instance: AudioSetResponse = AudioSetResponse { - state: ::protobuf::MessageField::none(), - error: ::protobuf::MessageField::none(), - special_fields: ::protobuf::SpecialFields::new(), - }; - &instance - } -} - -impl ::protobuf::MessageFull for AudioSetResponse { - fn descriptor() -> ::protobuf::reflect::MessageDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().message_by_package_relative_name("AudioSetResponse").unwrap()).clone() - } -} - -impl ::std::fmt::Display for AudioSetResponse { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - ::protobuf::text_format::fmt(self, f) - } -} - -impl ::protobuf::reflect::ProtobufValue for AudioSetResponse { - type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.GuestCapability) -pub enum GuestCapability { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_UNSPECIFIED) - GUEST_CAPABILITY_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_HEALTH) - GUEST_CAPABILITY_HEALTH = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_CAPABILITIES) - GUEST_CAPABILITY_CAPABILITIES = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_EXEC_ATTACHED) - GUEST_CAPABILITY_EXEC_ATTACHED = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_EXEC_DETACHED) - GUEST_CAPABILITY_EXEC_DETACHED = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_EXEC_TTY) - GUEST_CAPABILITY_EXEC_TTY = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_EXEC_LOGS) - GUEST_CAPABILITY_EXEC_LOGS = 6, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_TTY_RESIZE) - GUEST_CAPABILITY_TTY_RESIZE = 7, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_SIGNALS) - GUEST_CAPABILITY_SIGNALS = 8, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_READ_GUEST_FILE) - GUEST_CAPABILITY_READ_GUEST_FILE = 9, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_USBIP_IMPORT) - GUEST_CAPABILITY_USBIP_IMPORT = 10, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_SHELL_ATTACHED) - GUEST_CAPABILITY_SHELL_ATTACHED = 11, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_SHELL_MANAGEMENT) - GUEST_CAPABILITY_SHELL_MANAGEMENT = 12, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_SHELL_FORCE_ATTACH) - GUEST_CAPABILITY_SHELL_FORCE_ATTACH = 13, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_USBIP_STATUS) - GUEST_CAPABILITY_USBIP_STATUS = 14, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_SYSTEM_ACTIVATION) - GUEST_CAPABILITY_SYSTEM_ACTIVATION = 15, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_AUDIO_STATUS) - GUEST_CAPABILITY_AUDIO_STATUS = 16, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestCapability.GUEST_CAPABILITY_AUDIO_SET) - GUEST_CAPABILITY_AUDIO_SET = 17, -} - -impl ::protobuf::Enum for GuestCapability { - const NAME: &'static str = "GuestCapability"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_UNSPECIFIED), - 1 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_HEALTH), - 2 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_CAPABILITIES), - 3 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_ATTACHED), - 4 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_DETACHED), - 5 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_TTY), - 6 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_LOGS), - 7 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_TTY_RESIZE), - 8 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SIGNALS), - 9 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_READ_GUEST_FILE), - 10 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_USBIP_IMPORT), - 11 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SHELL_ATTACHED), - 12 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SHELL_MANAGEMENT), - 13 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SHELL_FORCE_ATTACH), - 14 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_USBIP_STATUS), - 15 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SYSTEM_ACTIVATION), - 16 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_AUDIO_STATUS), - 17 => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_AUDIO_SET), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "GUEST_CAPABILITY_UNSPECIFIED" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_UNSPECIFIED), - "GUEST_CAPABILITY_HEALTH" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_HEALTH), - "GUEST_CAPABILITY_CAPABILITIES" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_CAPABILITIES), - "GUEST_CAPABILITY_EXEC_ATTACHED" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_ATTACHED), - "GUEST_CAPABILITY_EXEC_DETACHED" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_DETACHED), - "GUEST_CAPABILITY_EXEC_TTY" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_TTY), - "GUEST_CAPABILITY_EXEC_LOGS" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_EXEC_LOGS), - "GUEST_CAPABILITY_TTY_RESIZE" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_TTY_RESIZE), - "GUEST_CAPABILITY_SIGNALS" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SIGNALS), - "GUEST_CAPABILITY_READ_GUEST_FILE" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_READ_GUEST_FILE), - "GUEST_CAPABILITY_USBIP_IMPORT" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_USBIP_IMPORT), - "GUEST_CAPABILITY_SHELL_ATTACHED" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SHELL_ATTACHED), - "GUEST_CAPABILITY_SHELL_MANAGEMENT" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SHELL_MANAGEMENT), - "GUEST_CAPABILITY_SHELL_FORCE_ATTACH" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SHELL_FORCE_ATTACH), - "GUEST_CAPABILITY_USBIP_STATUS" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_USBIP_STATUS), - "GUEST_CAPABILITY_SYSTEM_ACTIVATION" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_SYSTEM_ACTIVATION), - "GUEST_CAPABILITY_AUDIO_STATUS" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_AUDIO_STATUS), - "GUEST_CAPABILITY_AUDIO_SET" => ::std::option::Option::Some(GuestCapability::GUEST_CAPABILITY_AUDIO_SET), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [GuestCapability] = &[ - GuestCapability::GUEST_CAPABILITY_UNSPECIFIED, - GuestCapability::GUEST_CAPABILITY_HEALTH, - GuestCapability::GUEST_CAPABILITY_CAPABILITIES, - GuestCapability::GUEST_CAPABILITY_EXEC_ATTACHED, - GuestCapability::GUEST_CAPABILITY_EXEC_DETACHED, - GuestCapability::GUEST_CAPABILITY_EXEC_TTY, - GuestCapability::GUEST_CAPABILITY_EXEC_LOGS, - GuestCapability::GUEST_CAPABILITY_TTY_RESIZE, - GuestCapability::GUEST_CAPABILITY_SIGNALS, - GuestCapability::GUEST_CAPABILITY_READ_GUEST_FILE, - GuestCapability::GUEST_CAPABILITY_USBIP_IMPORT, - GuestCapability::GUEST_CAPABILITY_SHELL_ATTACHED, - GuestCapability::GUEST_CAPABILITY_SHELL_MANAGEMENT, - GuestCapability::GUEST_CAPABILITY_SHELL_FORCE_ATTACH, - GuestCapability::GUEST_CAPABILITY_USBIP_STATUS, - GuestCapability::GUEST_CAPABILITY_SYSTEM_ACTIVATION, - GuestCapability::GUEST_CAPABILITY_AUDIO_STATUS, - GuestCapability::GUEST_CAPABILITY_AUDIO_SET, - ]; -} - -impl ::protobuf::EnumFull for GuestCapability { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("GuestCapability").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for GuestCapability { - fn default() -> Self { - GuestCapability::GUEST_CAPABILITY_UNSPECIFIED - } -} - -impl GuestCapability { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("GuestCapability") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.GuestSubsystem) -pub enum GuestSubsystem { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_UNSPECIFIED) - GUEST_SUBSYSTEM_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_GUESTD) - GUEST_SUBSYSTEM_GUESTD = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_USERD) - GUEST_SUBSYSTEM_USERD = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_EXEC) - GUEST_SUBSYSTEM_EXEC = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_LOG_STORAGE) - GUEST_SUBSYSTEM_LOG_STORAGE = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_TOKEN) - GUEST_SUBSYSTEM_TOKEN = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_VSOCK) - GUEST_SUBSYSTEM_VSOCK = 6, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_USBIP) - GUEST_SUBSYSTEM_USBIP = 7, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_SHELL) - GUEST_SUBSYSTEM_SHELL = 8, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_SHPOOL) - GUEST_SUBSYSTEM_SHPOOL = 9, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_SYSTEM_ACTIVATION) - GUEST_SUBSYSTEM_SYSTEM_ACTIVATION = 10, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestSubsystem.GUEST_SUBSYSTEM_AUDIO) - GUEST_SUBSYSTEM_AUDIO = 11, -} - -impl ::protobuf::Enum for GuestSubsystem { - const NAME: &'static str = "GuestSubsystem"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_UNSPECIFIED), - 1 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_GUESTD), - 2 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_USERD), - 3 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_EXEC), - 4 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_LOG_STORAGE), - 5 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_TOKEN), - 6 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_VSOCK), - 7 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_USBIP), - 8 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_SHELL), - 9 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_SHPOOL), - 10 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_SYSTEM_ACTIVATION), - 11 => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_AUDIO), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "GUEST_SUBSYSTEM_UNSPECIFIED" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_UNSPECIFIED), - "GUEST_SUBSYSTEM_GUESTD" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_GUESTD), - "GUEST_SUBSYSTEM_USERD" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_USERD), - "GUEST_SUBSYSTEM_EXEC" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_EXEC), - "GUEST_SUBSYSTEM_LOG_STORAGE" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_LOG_STORAGE), - "GUEST_SUBSYSTEM_TOKEN" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_TOKEN), - "GUEST_SUBSYSTEM_VSOCK" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_VSOCK), - "GUEST_SUBSYSTEM_USBIP" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_USBIP), - "GUEST_SUBSYSTEM_SHELL" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_SHELL), - "GUEST_SUBSYSTEM_SHPOOL" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_SHPOOL), - "GUEST_SUBSYSTEM_SYSTEM_ACTIVATION" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_SYSTEM_ACTIVATION), - "GUEST_SUBSYSTEM_AUDIO" => ::std::option::Option::Some(GuestSubsystem::GUEST_SUBSYSTEM_AUDIO), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [GuestSubsystem] = &[ - GuestSubsystem::GUEST_SUBSYSTEM_UNSPECIFIED, - GuestSubsystem::GUEST_SUBSYSTEM_GUESTD, - GuestSubsystem::GUEST_SUBSYSTEM_USERD, - GuestSubsystem::GUEST_SUBSYSTEM_EXEC, - GuestSubsystem::GUEST_SUBSYSTEM_LOG_STORAGE, - GuestSubsystem::GUEST_SUBSYSTEM_TOKEN, - GuestSubsystem::GUEST_SUBSYSTEM_VSOCK, - GuestSubsystem::GUEST_SUBSYSTEM_USBIP, - GuestSubsystem::GUEST_SUBSYSTEM_SHELL, - GuestSubsystem::GUEST_SUBSYSTEM_SHPOOL, - GuestSubsystem::GUEST_SUBSYSTEM_SYSTEM_ACTIVATION, - GuestSubsystem::GUEST_SUBSYSTEM_AUDIO, - ]; -} - -impl ::protobuf::EnumFull for GuestSubsystem { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("GuestSubsystem").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for GuestSubsystem { - fn default() -> Self { - GuestSubsystem::GUEST_SUBSYSTEM_UNSPECIFIED - } -} - -impl GuestSubsystem { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("GuestSubsystem") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.TerminalKind) -pub enum TerminalKind { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.TerminalKind.TERMINAL_KIND_UNSPECIFIED) - TERMINAL_KIND_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.TerminalKind.TERMINAL_KIND_EXEC) - TERMINAL_KIND_EXEC = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.TerminalKind.TERMINAL_KIND_SHELL) - TERMINAL_KIND_SHELL = 2, -} - -impl ::protobuf::Enum for TerminalKind { - const NAME: &'static str = "TerminalKind"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(TerminalKind::TERMINAL_KIND_UNSPECIFIED), - 1 => ::std::option::Option::Some(TerminalKind::TERMINAL_KIND_EXEC), - 2 => ::std::option::Option::Some(TerminalKind::TERMINAL_KIND_SHELL), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "TERMINAL_KIND_UNSPECIFIED" => ::std::option::Option::Some(TerminalKind::TERMINAL_KIND_UNSPECIFIED), - "TERMINAL_KIND_EXEC" => ::std::option::Option::Some(TerminalKind::TERMINAL_KIND_EXEC), - "TERMINAL_KIND_SHELL" => ::std::option::Option::Some(TerminalKind::TERMINAL_KIND_SHELL), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [TerminalKind] = &[ - TerminalKind::TERMINAL_KIND_UNSPECIFIED, - TerminalKind::TERMINAL_KIND_EXEC, - TerminalKind::TERMINAL_KIND_SHELL, - ]; -} - -impl ::protobuf::EnumFull for TerminalKind { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("TerminalKind").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for TerminalKind { - fn default() -> Self { - TerminalKind::TERMINAL_KIND_UNSPECIFIED - } -} - -impl TerminalKind { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("TerminalKind") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.GuestActivationMode) -pub enum GuestActivationMode { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationMode.GUEST_ACTIVATION_MODE_UNSPECIFIED) - GUEST_ACTIVATION_MODE_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationMode.GUEST_ACTIVATION_MODE_SWITCH) - GUEST_ACTIVATION_MODE_SWITCH = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationMode.GUEST_ACTIVATION_MODE_BOOT) - GUEST_ACTIVATION_MODE_BOOT = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationMode.GUEST_ACTIVATION_MODE_TEST) - GUEST_ACTIVATION_MODE_TEST = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationMode.GUEST_ACTIVATION_MODE_DRY_ACTIVATE) - GUEST_ACTIVATION_MODE_DRY_ACTIVATE = 4, -} - -impl ::protobuf::Enum for GuestActivationMode { - const NAME: &'static str = "GuestActivationMode"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_UNSPECIFIED), - 1 => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_SWITCH), - 2 => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_BOOT), - 3 => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_TEST), - 4 => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_DRY_ACTIVATE), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "GUEST_ACTIVATION_MODE_UNSPECIFIED" => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_UNSPECIFIED), - "GUEST_ACTIVATION_MODE_SWITCH" => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_SWITCH), - "GUEST_ACTIVATION_MODE_BOOT" => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_BOOT), - "GUEST_ACTIVATION_MODE_TEST" => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_TEST), - "GUEST_ACTIVATION_MODE_DRY_ACTIVATE" => ::std::option::Option::Some(GuestActivationMode::GUEST_ACTIVATION_MODE_DRY_ACTIVATE), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [GuestActivationMode] = &[ - GuestActivationMode::GUEST_ACTIVATION_MODE_UNSPECIFIED, - GuestActivationMode::GUEST_ACTIVATION_MODE_SWITCH, - GuestActivationMode::GUEST_ACTIVATION_MODE_BOOT, - GuestActivationMode::GUEST_ACTIVATION_MODE_TEST, - GuestActivationMode::GUEST_ACTIVATION_MODE_DRY_ACTIVATE, - ]; -} - -impl ::protobuf::EnumFull for GuestActivationMode { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("GuestActivationMode").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for GuestActivationMode { - fn default() -> Self { - GuestActivationMode::GUEST_ACTIVATION_MODE_UNSPECIFIED - } -} - -impl GuestActivationMode { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("GuestActivationMode") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.GuestActivationState) -pub enum GuestActivationState { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationState.GUEST_ACTIVATION_STATE_UNSPECIFIED) - GUEST_ACTIVATION_STATE_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationState.GUEST_ACTIVATION_STATE_RUNNING) - GUEST_ACTIVATION_STATE_RUNNING = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationState.GUEST_ACTIVATION_STATE_SUCCEEDED) - GUEST_ACTIVATION_STATE_SUCCEEDED = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationState.GUEST_ACTIVATION_STATE_FAILED) - GUEST_ACTIVATION_STATE_FAILED = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationState.GUEST_ACTIVATION_STATE_TIMED_OUT) - GUEST_ACTIVATION_STATE_TIMED_OUT = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestActivationState.GUEST_ACTIVATION_STATE_LOST) - GUEST_ACTIVATION_STATE_LOST = 5, -} - -impl ::protobuf::Enum for GuestActivationState { - const NAME: &'static str = "GuestActivationState"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED), - 1 => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_RUNNING), - 2 => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_SUCCEEDED), - 3 => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_FAILED), - 4 => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_TIMED_OUT), - 5 => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_LOST), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "GUEST_ACTIVATION_STATE_UNSPECIFIED" => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED), - "GUEST_ACTIVATION_STATE_RUNNING" => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_RUNNING), - "GUEST_ACTIVATION_STATE_SUCCEEDED" => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_SUCCEEDED), - "GUEST_ACTIVATION_STATE_FAILED" => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_FAILED), - "GUEST_ACTIVATION_STATE_TIMED_OUT" => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_TIMED_OUT), - "GUEST_ACTIVATION_STATE_LOST" => ::std::option::Option::Some(GuestActivationState::GUEST_ACTIVATION_STATE_LOST), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [GuestActivationState] = &[ - GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED, - GuestActivationState::GUEST_ACTIVATION_STATE_RUNNING, - GuestActivationState::GUEST_ACTIVATION_STATE_SUCCEEDED, - GuestActivationState::GUEST_ACTIVATION_STATE_FAILED, - GuestActivationState::GUEST_ACTIVATION_STATE_TIMED_OUT, - GuestActivationState::GUEST_ACTIVATION_STATE_LOST, - ]; -} - -impl ::protobuf::EnumFull for GuestActivationState { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("GuestActivationState").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for GuestActivationState { - fn default() -> Self { - GuestActivationState::GUEST_ACTIVATION_STATE_UNSPECIFIED - } -} - -impl GuestActivationState { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("GuestActivationState") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.ShellState) -pub enum ShellState { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellState.SHELL_STATE_UNSPECIFIED) - SHELL_STATE_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellState.SHELL_STATE_ATTACHED) - SHELL_STATE_ATTACHED = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellState.SHELL_STATE_DETACHED) - SHELL_STATE_DETACHED = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellState.SHELL_STATE_KILLED) - SHELL_STATE_KILLED = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellState.SHELL_STATE_POOL_UNAVAILABLE) - SHELL_STATE_POOL_UNAVAILABLE = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellState.SHELL_STATE_FEATURE_DISABLED) - SHELL_STATE_FEATURE_DISABLED = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellState.SHELL_STATE_OUTPUT_GAP) - SHELL_STATE_OUTPUT_GAP = 6, -} - -impl ::protobuf::Enum for ShellState { - const NAME: &'static str = "ShellState"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(ShellState::SHELL_STATE_UNSPECIFIED), - 1 => ::std::option::Option::Some(ShellState::SHELL_STATE_ATTACHED), - 2 => ::std::option::Option::Some(ShellState::SHELL_STATE_DETACHED), - 3 => ::std::option::Option::Some(ShellState::SHELL_STATE_KILLED), - 4 => ::std::option::Option::Some(ShellState::SHELL_STATE_POOL_UNAVAILABLE), - 5 => ::std::option::Option::Some(ShellState::SHELL_STATE_FEATURE_DISABLED), - 6 => ::std::option::Option::Some(ShellState::SHELL_STATE_OUTPUT_GAP), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "SHELL_STATE_UNSPECIFIED" => ::std::option::Option::Some(ShellState::SHELL_STATE_UNSPECIFIED), - "SHELL_STATE_ATTACHED" => ::std::option::Option::Some(ShellState::SHELL_STATE_ATTACHED), - "SHELL_STATE_DETACHED" => ::std::option::Option::Some(ShellState::SHELL_STATE_DETACHED), - "SHELL_STATE_KILLED" => ::std::option::Option::Some(ShellState::SHELL_STATE_KILLED), - "SHELL_STATE_POOL_UNAVAILABLE" => ::std::option::Option::Some(ShellState::SHELL_STATE_POOL_UNAVAILABLE), - "SHELL_STATE_FEATURE_DISABLED" => ::std::option::Option::Some(ShellState::SHELL_STATE_FEATURE_DISABLED), - "SHELL_STATE_OUTPUT_GAP" => ::std::option::Option::Some(ShellState::SHELL_STATE_OUTPUT_GAP), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [ShellState] = &[ - ShellState::SHELL_STATE_UNSPECIFIED, - ShellState::SHELL_STATE_ATTACHED, - ShellState::SHELL_STATE_DETACHED, - ShellState::SHELL_STATE_KILLED, - ShellState::SHELL_STATE_POOL_UNAVAILABLE, - ShellState::SHELL_STATE_FEATURE_DISABLED, - ShellState::SHELL_STATE_OUTPUT_GAP, - ]; -} - -impl ::protobuf::EnumFull for ShellState { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("ShellState").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for ShellState { - fn default() -> Self { - ShellState::SHELL_STATE_UNSPECIFIED - } -} - -impl ShellState { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("ShellState") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.ShellCloseCause) -pub enum ShellCloseCause { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellCloseCause.SHELL_CLOSE_CAUSE_UNSPECIFIED) - SHELL_CLOSE_CAUSE_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellCloseCause.SHELL_CLOSE_CAUSE_CLIENT_DETACH) - SHELL_CLOSE_CAUSE_CLIENT_DETACH = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellCloseCause.SHELL_CLOSE_CAUSE_EVICTED_BY_FORCE) - SHELL_CLOSE_CAUSE_EVICTED_BY_FORCE = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellCloseCause.SHELL_CLOSE_CAUSE_EVICTED_BY_ADMIN_DETACH) - SHELL_CLOSE_CAUSE_EVICTED_BY_ADMIN_DETACH = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellCloseCause.SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN) - SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellCloseCause.SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE) - SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ShellCloseCause.SHELL_CLOSE_CAUSE_OUTPUT_GAP) - SHELL_CLOSE_CAUSE_OUTPUT_GAP = 6, -} - -impl ::protobuf::Enum for ShellCloseCause { - const NAME: &'static str = "ShellCloseCause"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_UNSPECIFIED), - 1 => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_CLIENT_DETACH), - 2 => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_EVICTED_BY_FORCE), - 3 => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_EVICTED_BY_ADMIN_DETACH), - 4 => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN), - 5 => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE), - 6 => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_OUTPUT_GAP), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "SHELL_CLOSE_CAUSE_UNSPECIFIED" => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_UNSPECIFIED), - "SHELL_CLOSE_CAUSE_CLIENT_DETACH" => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_CLIENT_DETACH), - "SHELL_CLOSE_CAUSE_EVICTED_BY_FORCE" => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_EVICTED_BY_FORCE), - "SHELL_CLOSE_CAUSE_EVICTED_BY_ADMIN_DETACH" => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_EVICTED_BY_ADMIN_DETACH), - "SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN" => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN), - "SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE" => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE), - "SHELL_CLOSE_CAUSE_OUTPUT_GAP" => ::std::option::Option::Some(ShellCloseCause::SHELL_CLOSE_CAUSE_OUTPUT_GAP), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [ShellCloseCause] = &[ - ShellCloseCause::SHELL_CLOSE_CAUSE_UNSPECIFIED, - ShellCloseCause::SHELL_CLOSE_CAUSE_CLIENT_DETACH, - ShellCloseCause::SHELL_CLOSE_CAUSE_EVICTED_BY_FORCE, - ShellCloseCause::SHELL_CLOSE_CAUSE_EVICTED_BY_ADMIN_DETACH, - ShellCloseCause::SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN, - ShellCloseCause::SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE, - ShellCloseCause::SHELL_CLOSE_CAUSE_OUTPUT_GAP, - ]; -} - -impl ::protobuf::EnumFull for ShellCloseCause { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("ShellCloseCause").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for ShellCloseCause { - fn default() -> Self { - ShellCloseCause::SHELL_CLOSE_CAUSE_UNSPECIFIED - } -} - -impl ShellCloseCause { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("ShellCloseCause") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.GuestFileId) -pub enum GuestFileId { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestFileId.GUEST_FILE_ID_UNSPECIFIED) - GUEST_FILE_ID_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestFileId.GUEST_FILE_ID_GUEST_CONFIG) - GUEST_FILE_ID_GUEST_CONFIG = 1, -} - -impl ::protobuf::Enum for GuestFileId { - const NAME: &'static str = "GuestFileId"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(GuestFileId::GUEST_FILE_ID_UNSPECIFIED), - 1 => ::std::option::Option::Some(GuestFileId::GUEST_FILE_ID_GUEST_CONFIG), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "GUEST_FILE_ID_UNSPECIFIED" => ::std::option::Option::Some(GuestFileId::GUEST_FILE_ID_UNSPECIFIED), - "GUEST_FILE_ID_GUEST_CONFIG" => ::std::option::Option::Some(GuestFileId::GUEST_FILE_ID_GUEST_CONFIG), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [GuestFileId] = &[ - GuestFileId::GUEST_FILE_ID_UNSPECIFIED, - GuestFileId::GUEST_FILE_ID_GUEST_CONFIG, - ]; -} - -impl ::protobuf::EnumFull for GuestFileId { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("GuestFileId").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for GuestFileId { - fn default() -> Self { - GuestFileId::GUEST_FILE_ID_UNSPECIFIED - } -} - -impl GuestFileId { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("GuestFileId") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.UsbipImportAction) -pub enum UsbipImportAction { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.UsbipImportAction.USBIP_IMPORT_ACTION_UNSPECIFIED) - USBIP_IMPORT_ACTION_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.UsbipImportAction.USBIP_IMPORT_ACTION_ATTACH) - USBIP_IMPORT_ACTION_ATTACH = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.UsbipImportAction.USBIP_IMPORT_ACTION_DETACH) - USBIP_IMPORT_ACTION_DETACH = 2, -} - -impl ::protobuf::Enum for UsbipImportAction { - const NAME: &'static str = "UsbipImportAction"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED), - 1 => ::std::option::Option::Some(UsbipImportAction::USBIP_IMPORT_ACTION_ATTACH), - 2 => ::std::option::Option::Some(UsbipImportAction::USBIP_IMPORT_ACTION_DETACH), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "USBIP_IMPORT_ACTION_UNSPECIFIED" => ::std::option::Option::Some(UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED), - "USBIP_IMPORT_ACTION_ATTACH" => ::std::option::Option::Some(UsbipImportAction::USBIP_IMPORT_ACTION_ATTACH), - "USBIP_IMPORT_ACTION_DETACH" => ::std::option::Option::Some(UsbipImportAction::USBIP_IMPORT_ACTION_DETACH), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [UsbipImportAction] = &[ - UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED, - UsbipImportAction::USBIP_IMPORT_ACTION_ATTACH, - UsbipImportAction::USBIP_IMPORT_ACTION_DETACH, - ]; -} - -impl ::protobuf::EnumFull for UsbipImportAction { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("UsbipImportAction").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for UsbipImportAction { - fn default() -> Self { - UsbipImportAction::USBIP_IMPORT_ACTION_UNSPECIFIED - } -} - -impl UsbipImportAction { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("UsbipImportAction") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.HealthOrigin) -pub enum HealthOrigin { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthOrigin.HEALTH_ORIGIN_UNSPECIFIED) - HEALTH_ORIGIN_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthOrigin.HEALTH_ORIGIN_GUEST_REPORTED) - HEALTH_ORIGIN_GUEST_REPORTED = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthOrigin.HEALTH_ORIGIN_HOST_SYNTHESIZED) - HEALTH_ORIGIN_HOST_SYNTHESIZED = 2, -} - -impl ::protobuf::Enum for HealthOrigin { - const NAME: &'static str = "HealthOrigin"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(HealthOrigin::HEALTH_ORIGIN_UNSPECIFIED), - 1 => ::std::option::Option::Some(HealthOrigin::HEALTH_ORIGIN_GUEST_REPORTED), - 2 => ::std::option::Option::Some(HealthOrigin::HEALTH_ORIGIN_HOST_SYNTHESIZED), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "HEALTH_ORIGIN_UNSPECIFIED" => ::std::option::Option::Some(HealthOrigin::HEALTH_ORIGIN_UNSPECIFIED), - "HEALTH_ORIGIN_GUEST_REPORTED" => ::std::option::Option::Some(HealthOrigin::HEALTH_ORIGIN_GUEST_REPORTED), - "HEALTH_ORIGIN_HOST_SYNTHESIZED" => ::std::option::Option::Some(HealthOrigin::HEALTH_ORIGIN_HOST_SYNTHESIZED), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [HealthOrigin] = &[ - HealthOrigin::HEALTH_ORIGIN_UNSPECIFIED, - HealthOrigin::HEALTH_ORIGIN_GUEST_REPORTED, - HealthOrigin::HEALTH_ORIGIN_HOST_SYNTHESIZED, - ]; -} - -impl ::protobuf::EnumFull for HealthOrigin { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("HealthOrigin").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for HealthOrigin { - fn default() -> Self { - HealthOrigin::HEALTH_ORIGIN_UNSPECIFIED - } -} - -impl HealthOrigin { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("HealthOrigin") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.HealthState) -pub enum HealthState { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_UNSPECIFIED) - HEALTH_STATE_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_HEALTHY) - HEALTH_STATE_HEALTHY = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_DEGRADED) - HEALTH_STATE_DEGRADED = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_UNAVAILABLE_OLD_GENERATION) - HEALTH_STATE_UNAVAILABLE_OLD_GENERATION = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_LISTENER_ABSENT) - HEALTH_STATE_LISTENER_ABSENT = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_TRANSPORT_UNREACHABLE) - HEALTH_STATE_TRANSPORT_UNREACHABLE = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_AUTH_FAILED) - HEALTH_STATE_AUTH_FAILED = 6, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_PROTOCOL_MISMATCH) - HEALTH_STATE_PROTOCOL_MISMATCH = 7, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthState.HEALTH_STATE_STALE_SESSION) - HEALTH_STATE_STALE_SESSION = 8, -} - -impl ::protobuf::Enum for HealthState { - const NAME: &'static str = "HealthState"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(HealthState::HEALTH_STATE_UNSPECIFIED), - 1 => ::std::option::Option::Some(HealthState::HEALTH_STATE_HEALTHY), - 2 => ::std::option::Option::Some(HealthState::HEALTH_STATE_DEGRADED), - 3 => ::std::option::Option::Some(HealthState::HEALTH_STATE_UNAVAILABLE_OLD_GENERATION), - 4 => ::std::option::Option::Some(HealthState::HEALTH_STATE_LISTENER_ABSENT), - 5 => ::std::option::Option::Some(HealthState::HEALTH_STATE_TRANSPORT_UNREACHABLE), - 6 => ::std::option::Option::Some(HealthState::HEALTH_STATE_AUTH_FAILED), - 7 => ::std::option::Option::Some(HealthState::HEALTH_STATE_PROTOCOL_MISMATCH), - 8 => ::std::option::Option::Some(HealthState::HEALTH_STATE_STALE_SESSION), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "HEALTH_STATE_UNSPECIFIED" => ::std::option::Option::Some(HealthState::HEALTH_STATE_UNSPECIFIED), - "HEALTH_STATE_HEALTHY" => ::std::option::Option::Some(HealthState::HEALTH_STATE_HEALTHY), - "HEALTH_STATE_DEGRADED" => ::std::option::Option::Some(HealthState::HEALTH_STATE_DEGRADED), - "HEALTH_STATE_UNAVAILABLE_OLD_GENERATION" => ::std::option::Option::Some(HealthState::HEALTH_STATE_UNAVAILABLE_OLD_GENERATION), - "HEALTH_STATE_LISTENER_ABSENT" => ::std::option::Option::Some(HealthState::HEALTH_STATE_LISTENER_ABSENT), - "HEALTH_STATE_TRANSPORT_UNREACHABLE" => ::std::option::Option::Some(HealthState::HEALTH_STATE_TRANSPORT_UNREACHABLE), - "HEALTH_STATE_AUTH_FAILED" => ::std::option::Option::Some(HealthState::HEALTH_STATE_AUTH_FAILED), - "HEALTH_STATE_PROTOCOL_MISMATCH" => ::std::option::Option::Some(HealthState::HEALTH_STATE_PROTOCOL_MISMATCH), - "HEALTH_STATE_STALE_SESSION" => ::std::option::Option::Some(HealthState::HEALTH_STATE_STALE_SESSION), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [HealthState] = &[ - HealthState::HEALTH_STATE_UNSPECIFIED, - HealthState::HEALTH_STATE_HEALTHY, - HealthState::HEALTH_STATE_DEGRADED, - HealthState::HEALTH_STATE_UNAVAILABLE_OLD_GENERATION, - HealthState::HEALTH_STATE_LISTENER_ABSENT, - HealthState::HEALTH_STATE_TRANSPORT_UNREACHABLE, - HealthState::HEALTH_STATE_AUTH_FAILED, - HealthState::HEALTH_STATE_PROTOCOL_MISMATCH, - HealthState::HEALTH_STATE_STALE_SESSION, - ]; -} - -impl ::protobuf::EnumFull for HealthState { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("HealthState").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for HealthState { - fn default() -> Self { - HealthState::HEALTH_STATE_UNSPECIFIED - } -} - -impl HealthState { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("HealthState") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.HealthReason) -pub enum HealthReason { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_UNSPECIFIED) - HEALTH_REASON_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_NONE) - HEALTH_REASON_NONE = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_OLD_GENERATION) - HEALTH_REASON_OLD_GENERATION = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_LISTENER_ABSENT) - HEALTH_REASON_LISTENER_ABSENT = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_CONNECT_REFUSED) - HEALTH_REASON_CONNECT_REFUSED = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_CONNECT_TIMEOUT) - HEALTH_REASON_CONNECT_TIMEOUT = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_EOF_BEFORE_ACK) - HEALTH_REASON_EOF_BEFORE_ACK = 6, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_MALFORMED_ACK) - HEALTH_REASON_MALFORMED_ACK = 7, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_ACK_TOO_LONG) - HEALTH_REASON_ACK_TOO_LONG = 8, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_TRANSPORT_IO) - HEALTH_REASON_TRANSPORT_IO = 9, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_AUTH_TOKEN_REJECTED) - HEALTH_REASON_AUTH_TOKEN_REJECTED = 10, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_PROTOCOL_VERSION_UNSUPPORTED) - HEALTH_REASON_PROTOCOL_VERSION_UNSUPPORTED = 11, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_SESSION_GENERATION_MISMATCH) - HEALTH_REASON_SESSION_GENERATION_MISMATCH = 12, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABLE) - HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABLE = 13, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_LOG_STORAGE_UNAVAILABLE) - HEALTH_REASON_LOG_STORAGE_UNAVAILABLE = 14, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_QUOTA_EXCEEDED) - HEALTH_REASON_QUOTA_EXCEEDED = 15, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_RATE_LIMITED) - HEALTH_REASON_RATE_LIMITED = 16, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthReason.HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED) - HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED = 17, -} - -impl ::protobuf::Enum for HealthReason { - const NAME: &'static str = "HealthReason"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_UNSPECIFIED), - 1 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_NONE), - 2 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_OLD_GENERATION), - 3 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_LISTENER_ABSENT), - 4 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_CONNECT_REFUSED), - 5 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_CONNECT_TIMEOUT), - 6 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_EOF_BEFORE_ACK), - 7 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_MALFORMED_ACK), - 8 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_ACK_TOO_LONG), - 9 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_TRANSPORT_IO), - 10 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_AUTH_TOKEN_REJECTED), - 11 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_PROTOCOL_VERSION_UNSUPPORTED), - 12 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_SESSION_GENERATION_MISMATCH), - 13 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABLE), - 14 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_LOG_STORAGE_UNAVAILABLE), - 15 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_QUOTA_EXCEEDED), - 16 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_RATE_LIMITED), - 17 => ::std::option::Option::Some(HealthReason::HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "HEALTH_REASON_UNSPECIFIED" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_UNSPECIFIED), - "HEALTH_REASON_NONE" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_NONE), - "HEALTH_REASON_OLD_GENERATION" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_OLD_GENERATION), - "HEALTH_REASON_LISTENER_ABSENT" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_LISTENER_ABSENT), - "HEALTH_REASON_CONNECT_REFUSED" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_CONNECT_REFUSED), - "HEALTH_REASON_CONNECT_TIMEOUT" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_CONNECT_TIMEOUT), - "HEALTH_REASON_EOF_BEFORE_ACK" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_EOF_BEFORE_ACK), - "HEALTH_REASON_MALFORMED_ACK" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_MALFORMED_ACK), - "HEALTH_REASON_ACK_TOO_LONG" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_ACK_TOO_LONG), - "HEALTH_REASON_TRANSPORT_IO" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_TRANSPORT_IO), - "HEALTH_REASON_AUTH_TOKEN_REJECTED" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_AUTH_TOKEN_REJECTED), - "HEALTH_REASON_PROTOCOL_VERSION_UNSUPPORTED" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_PROTOCOL_VERSION_UNSUPPORTED), - "HEALTH_REASON_SESSION_GENERATION_MISMATCH" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_SESSION_GENERATION_MISMATCH), - "HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABLE" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABLE), - "HEALTH_REASON_LOG_STORAGE_UNAVAILABLE" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_LOG_STORAGE_UNAVAILABLE), - "HEALTH_REASON_QUOTA_EXCEEDED" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_QUOTA_EXCEEDED), - "HEALTH_REASON_RATE_LIMITED" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_RATE_LIMITED), - "HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED" => ::std::option::Option::Some(HealthReason::HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [HealthReason] = &[ - HealthReason::HEALTH_REASON_UNSPECIFIED, - HealthReason::HEALTH_REASON_NONE, - HealthReason::HEALTH_REASON_OLD_GENERATION, - HealthReason::HEALTH_REASON_LISTENER_ABSENT, - HealthReason::HEALTH_REASON_CONNECT_REFUSED, - HealthReason::HEALTH_REASON_CONNECT_TIMEOUT, - HealthReason::HEALTH_REASON_EOF_BEFORE_ACK, - HealthReason::HEALTH_REASON_MALFORMED_ACK, - HealthReason::HEALTH_REASON_ACK_TOO_LONG, - HealthReason::HEALTH_REASON_TRANSPORT_IO, - HealthReason::HEALTH_REASON_AUTH_TOKEN_REJECTED, - HealthReason::HEALTH_REASON_PROTOCOL_VERSION_UNSUPPORTED, - HealthReason::HEALTH_REASON_SESSION_GENERATION_MISMATCH, - HealthReason::HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABLE, - HealthReason::HEALTH_REASON_LOG_STORAGE_UNAVAILABLE, - HealthReason::HEALTH_REASON_QUOTA_EXCEEDED, - HealthReason::HEALTH_REASON_RATE_LIMITED, - HealthReason::HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED, - ]; -} - -impl ::protobuf::EnumFull for HealthReason { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("HealthReason").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for HealthReason { - fn default() -> Self { - HealthReason::HEALTH_REASON_UNSPECIFIED - } -} - -impl HealthReason { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("HealthReason") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.HealthRemediation) -pub enum HealthRemediation { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_UNSPECIFIED) - HEALTH_REMEDIATION_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_NONE) - HEALTH_REMEDIATION_NONE = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_RETRY) - HEALTH_REMEDIATION_RETRY = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_RESTART_VM) - HEALTH_REMEDIATION_RESTART_VM = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_UPGRADE_GUEST) - HEALTH_REMEDIATION_UPGRADE_GUEST = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_CHECK_AUTH_TOKEN) - HEALTH_REMEDIATION_CHECK_AUTH_TOKEN = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_CHECK_GUESTD_SERVICE) - HEALTH_REMEDIATION_CHECK_GUESTD_SERVICE = 6, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_REDUCE_LOAD) - HEALTH_REMEDIATION_REDUCE_LOAD = 7, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.HealthRemediation.HEALTH_REMEDIATION_INSPECT_GUEST_LOGS) - HEALTH_REMEDIATION_INSPECT_GUEST_LOGS = 8, -} - -impl ::protobuf::Enum for HealthRemediation { - const NAME: &'static str = "HealthRemediation"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED), - 1 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_NONE), - 2 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_RETRY), - 3 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_RESTART_VM), - 4 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_UPGRADE_GUEST), - 5 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_CHECK_AUTH_TOKEN), - 6 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_CHECK_GUESTD_SERVICE), - 7 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_REDUCE_LOAD), - 8 => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_INSPECT_GUEST_LOGS), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "HEALTH_REMEDIATION_UNSPECIFIED" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED), - "HEALTH_REMEDIATION_NONE" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_NONE), - "HEALTH_REMEDIATION_RETRY" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_RETRY), - "HEALTH_REMEDIATION_RESTART_VM" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_RESTART_VM), - "HEALTH_REMEDIATION_UPGRADE_GUEST" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_UPGRADE_GUEST), - "HEALTH_REMEDIATION_CHECK_AUTH_TOKEN" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_CHECK_AUTH_TOKEN), - "HEALTH_REMEDIATION_CHECK_GUESTD_SERVICE" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_CHECK_GUESTD_SERVICE), - "HEALTH_REMEDIATION_REDUCE_LOAD" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_REDUCE_LOAD), - "HEALTH_REMEDIATION_INSPECT_GUEST_LOGS" => ::std::option::Option::Some(HealthRemediation::HEALTH_REMEDIATION_INSPECT_GUEST_LOGS), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [HealthRemediation] = &[ - HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED, - HealthRemediation::HEALTH_REMEDIATION_NONE, - HealthRemediation::HEALTH_REMEDIATION_RETRY, - HealthRemediation::HEALTH_REMEDIATION_RESTART_VM, - HealthRemediation::HEALTH_REMEDIATION_UPGRADE_GUEST, - HealthRemediation::HEALTH_REMEDIATION_CHECK_AUTH_TOKEN, - HealthRemediation::HEALTH_REMEDIATION_CHECK_GUESTD_SERVICE, - HealthRemediation::HEALTH_REMEDIATION_REDUCE_LOAD, - HealthRemediation::HEALTH_REMEDIATION_INSPECT_GUEST_LOGS, - ]; -} - -impl ::protobuf::EnumFull for HealthRemediation { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("HealthRemediation").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for HealthRemediation { - fn default() -> Self { - HealthRemediation::HEALTH_REMEDIATION_UNSPECIFIED - } -} - -impl HealthRemediation { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("HealthRemediation") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.OutputStream) -pub enum OutputStream { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.OutputStream.OUTPUT_STREAM_UNSPECIFIED) - OUTPUT_STREAM_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.OutputStream.OUTPUT_STREAM_STDOUT) - OUTPUT_STREAM_STDOUT = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.OutputStream.OUTPUT_STREAM_STDERR) - OUTPUT_STREAM_STDERR = 2, -} - -impl ::protobuf::Enum for OutputStream { - const NAME: &'static str = "OutputStream"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(OutputStream::OUTPUT_STREAM_UNSPECIFIED), - 1 => ::std::option::Option::Some(OutputStream::OUTPUT_STREAM_STDOUT), - 2 => ::std::option::Option::Some(OutputStream::OUTPUT_STREAM_STDERR), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "OUTPUT_STREAM_UNSPECIFIED" => ::std::option::Option::Some(OutputStream::OUTPUT_STREAM_UNSPECIFIED), - "OUTPUT_STREAM_STDOUT" => ::std::option::Option::Some(OutputStream::OUTPUT_STREAM_STDOUT), - "OUTPUT_STREAM_STDERR" => ::std::option::Option::Some(OutputStream::OUTPUT_STREAM_STDERR), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [OutputStream] = &[ - OutputStream::OUTPUT_STREAM_UNSPECIFIED, - OutputStream::OUTPUT_STREAM_STDOUT, - OutputStream::OUTPUT_STREAM_STDERR, - ]; -} - -impl ::protobuf::EnumFull for OutputStream { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("OutputStream").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for OutputStream { - fn default() -> Self { - OutputStream::OUTPUT_STREAM_UNSPECIFIED - } -} - -impl OutputStream { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("OutputStream") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.WriteDisposition) -pub enum WriteDisposition { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.WriteDisposition.WRITE_DISPOSITION_UNSPECIFIED) - WRITE_DISPOSITION_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.WriteDisposition.WRITE_DISPOSITION_ACCEPTED) - WRITE_DISPOSITION_ACCEPTED = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.WriteDisposition.WRITE_DISPOSITION_DUPLICATE) - WRITE_DISPOSITION_DUPLICATE = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.WriteDisposition.WRITE_DISPOSITION_REJECTED) - WRITE_DISPOSITION_REJECTED = 3, -} - -impl ::protobuf::Enum for WriteDisposition { - const NAME: &'static str = "WriteDisposition"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED), - 1 => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_ACCEPTED), - 2 => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_DUPLICATE), - 3 => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_REJECTED), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "WRITE_DISPOSITION_UNSPECIFIED" => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED), - "WRITE_DISPOSITION_ACCEPTED" => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_ACCEPTED), - "WRITE_DISPOSITION_DUPLICATE" => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_DUPLICATE), - "WRITE_DISPOSITION_REJECTED" => ::std::option::Option::Some(WriteDisposition::WRITE_DISPOSITION_REJECTED), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [WriteDisposition] = &[ - WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED, - WriteDisposition::WRITE_DISPOSITION_ACCEPTED, - WriteDisposition::WRITE_DISPOSITION_DUPLICATE, - WriteDisposition::WRITE_DISPOSITION_REJECTED, - ]; -} - -impl ::protobuf::EnumFull for WriteDisposition { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("WriteDisposition").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for WriteDisposition { - fn default() -> Self { - WriteDisposition::WRITE_DISPOSITION_UNSPECIFIED - } -} - -impl WriteDisposition { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("WriteDisposition") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.ExecState) -pub enum ExecState { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_UNSPECIFIED) - EXEC_STATE_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_CREATED) - EXEC_STATE_CREATED = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_RUNNING) - EXEC_STATE_RUNNING = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_EXITED) - EXEC_STATE_EXITED = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_SIGNALED) - EXEC_STATE_SIGNALED = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_CANCELLED) - EXEC_STATE_CANCELLED = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_SLOW_CONSUMER_CANCELLED) - EXEC_STATE_SLOW_CONSUMER_CANCELLED = 6, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_PROTOCOL_ERROR) - EXEC_STATE_PROTOCOL_ERROR = 7, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_LOST_GUESTD) - EXEC_STATE_LOST_GUESTD = 8, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecState.EXEC_STATE_REAPED) - EXEC_STATE_REAPED = 9, -} - -impl ::protobuf::Enum for ExecState { - const NAME: &'static str = "ExecState"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(ExecState::EXEC_STATE_UNSPECIFIED), - 1 => ::std::option::Option::Some(ExecState::EXEC_STATE_CREATED), - 2 => ::std::option::Option::Some(ExecState::EXEC_STATE_RUNNING), - 3 => ::std::option::Option::Some(ExecState::EXEC_STATE_EXITED), - 4 => ::std::option::Option::Some(ExecState::EXEC_STATE_SIGNALED), - 5 => ::std::option::Option::Some(ExecState::EXEC_STATE_CANCELLED), - 6 => ::std::option::Option::Some(ExecState::EXEC_STATE_SLOW_CONSUMER_CANCELLED), - 7 => ::std::option::Option::Some(ExecState::EXEC_STATE_PROTOCOL_ERROR), - 8 => ::std::option::Option::Some(ExecState::EXEC_STATE_LOST_GUESTD), - 9 => ::std::option::Option::Some(ExecState::EXEC_STATE_REAPED), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "EXEC_STATE_UNSPECIFIED" => ::std::option::Option::Some(ExecState::EXEC_STATE_UNSPECIFIED), - "EXEC_STATE_CREATED" => ::std::option::Option::Some(ExecState::EXEC_STATE_CREATED), - "EXEC_STATE_RUNNING" => ::std::option::Option::Some(ExecState::EXEC_STATE_RUNNING), - "EXEC_STATE_EXITED" => ::std::option::Option::Some(ExecState::EXEC_STATE_EXITED), - "EXEC_STATE_SIGNALED" => ::std::option::Option::Some(ExecState::EXEC_STATE_SIGNALED), - "EXEC_STATE_CANCELLED" => ::std::option::Option::Some(ExecState::EXEC_STATE_CANCELLED), - "EXEC_STATE_SLOW_CONSUMER_CANCELLED" => ::std::option::Option::Some(ExecState::EXEC_STATE_SLOW_CONSUMER_CANCELLED), - "EXEC_STATE_PROTOCOL_ERROR" => ::std::option::Option::Some(ExecState::EXEC_STATE_PROTOCOL_ERROR), - "EXEC_STATE_LOST_GUESTD" => ::std::option::Option::Some(ExecState::EXEC_STATE_LOST_GUESTD), - "EXEC_STATE_REAPED" => ::std::option::Option::Some(ExecState::EXEC_STATE_REAPED), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [ExecState] = &[ - ExecState::EXEC_STATE_UNSPECIFIED, - ExecState::EXEC_STATE_CREATED, - ExecState::EXEC_STATE_RUNNING, - ExecState::EXEC_STATE_EXITED, - ExecState::EXEC_STATE_SIGNALED, - ExecState::EXEC_STATE_CANCELLED, - ExecState::EXEC_STATE_SLOW_CONSUMER_CANCELLED, - ExecState::EXEC_STATE_PROTOCOL_ERROR, - ExecState::EXEC_STATE_LOST_GUESTD, - ExecState::EXEC_STATE_REAPED, - ]; -} - -impl ::protobuf::EnumFull for ExecState { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("ExecState").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for ExecState { - fn default() -> Self { - ExecState::EXEC_STATE_UNSPECIFIED - } -} - -impl ExecState { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("ExecState") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.StdinState) -pub enum StdinState { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.StdinState.STDIN_STATE_UNSPECIFIED) - STDIN_STATE_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.StdinState.STDIN_STATE_OPEN) - STDIN_STATE_OPEN = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.StdinState.STDIN_STATE_CLOSING) - STDIN_STATE_CLOSING = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.StdinState.STDIN_STATE_CLOSED) - STDIN_STATE_CLOSED = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.StdinState.STDIN_STATE_CLOSED_BY_PROCESS) - STDIN_STATE_CLOSED_BY_PROCESS = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.StdinState.STDIN_STATE_REJECTED_NOT_INTERACTIVE) - STDIN_STATE_REJECTED_NOT_INTERACTIVE = 5, -} - -impl ::protobuf::Enum for StdinState { - const NAME: &'static str = "StdinState"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(StdinState::STDIN_STATE_UNSPECIFIED), - 1 => ::std::option::Option::Some(StdinState::STDIN_STATE_OPEN), - 2 => ::std::option::Option::Some(StdinState::STDIN_STATE_CLOSING), - 3 => ::std::option::Option::Some(StdinState::STDIN_STATE_CLOSED), - 4 => ::std::option::Option::Some(StdinState::STDIN_STATE_CLOSED_BY_PROCESS), - 5 => ::std::option::Option::Some(StdinState::STDIN_STATE_REJECTED_NOT_INTERACTIVE), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "STDIN_STATE_UNSPECIFIED" => ::std::option::Option::Some(StdinState::STDIN_STATE_UNSPECIFIED), - "STDIN_STATE_OPEN" => ::std::option::Option::Some(StdinState::STDIN_STATE_OPEN), - "STDIN_STATE_CLOSING" => ::std::option::Option::Some(StdinState::STDIN_STATE_CLOSING), - "STDIN_STATE_CLOSED" => ::std::option::Option::Some(StdinState::STDIN_STATE_CLOSED), - "STDIN_STATE_CLOSED_BY_PROCESS" => ::std::option::Option::Some(StdinState::STDIN_STATE_CLOSED_BY_PROCESS), - "STDIN_STATE_REJECTED_NOT_INTERACTIVE" => ::std::option::Option::Some(StdinState::STDIN_STATE_REJECTED_NOT_INTERACTIVE), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [StdinState] = &[ - StdinState::STDIN_STATE_UNSPECIFIED, - StdinState::STDIN_STATE_OPEN, - StdinState::STDIN_STATE_CLOSING, - StdinState::STDIN_STATE_CLOSED, - StdinState::STDIN_STATE_CLOSED_BY_PROCESS, - StdinState::STDIN_STATE_REJECTED_NOT_INTERACTIVE, - ]; -} - -impl ::protobuf::EnumFull for StdinState { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("StdinState").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for StdinState { - fn default() -> Self { - StdinState::STDIN_STATE_UNSPECIFIED - } -} - -impl StdinState { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("StdinState") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.SignalTarget) -pub enum SignalTarget { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.SignalTarget.SIGNAL_TARGET_UNSPECIFIED) - SIGNAL_TARGET_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.SignalTarget.SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP) - SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.SignalTarget.SIGNAL_TARGET_PROCESS_TREE) - SIGNAL_TARGET_PROCESS_TREE = 2, -} - -impl ::protobuf::Enum for SignalTarget { - const NAME: &'static str = "SignalTarget"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(SignalTarget::SIGNAL_TARGET_UNSPECIFIED), - 1 => ::std::option::Option::Some(SignalTarget::SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP), - 2 => ::std::option::Option::Some(SignalTarget::SIGNAL_TARGET_PROCESS_TREE), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "SIGNAL_TARGET_UNSPECIFIED" => ::std::option::Option::Some(SignalTarget::SIGNAL_TARGET_UNSPECIFIED), - "SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP" => ::std::option::Option::Some(SignalTarget::SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP), - "SIGNAL_TARGET_PROCESS_TREE" => ::std::option::Option::Some(SignalTarget::SIGNAL_TARGET_PROCESS_TREE), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [SignalTarget] = &[ - SignalTarget::SIGNAL_TARGET_UNSPECIFIED, - SignalTarget::SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP, - SignalTarget::SIGNAL_TARGET_PROCESS_TREE, - ]; -} - -impl ::protobuf::EnumFull for SignalTarget { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("SignalTarget").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for SignalTarget { - fn default() -> Self { - SignalTarget::SIGNAL_TARGET_UNSPECIFIED - } -} - -impl SignalTarget { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("SignalTarget") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.ExecCancelReason) -pub enum ExecCancelReason { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecCancelReason.EXEC_CANCEL_REASON_UNSPECIFIED) - EXEC_CANCEL_REASON_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecCancelReason.EXEC_CANCEL_REASON_CLIENT_DISCONNECT) - EXEC_CANCEL_REASON_CLIENT_DISCONNECT = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecCancelReason.EXEC_CANCEL_REASON_USER_REQUESTED) - EXEC_CANCEL_REASON_USER_REQUESTED = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecCancelReason.EXEC_CANCEL_REASON_SLOW_CONSUMER) - EXEC_CANCEL_REASON_SLOW_CONSUMER = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.ExecCancelReason.EXEC_CANCEL_REASON_PROTOCOL_ERROR) - EXEC_CANCEL_REASON_PROTOCOL_ERROR = 4, -} - -impl ::protobuf::Enum for ExecCancelReason { - const NAME: &'static str = "ExecCancelReason"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_UNSPECIFIED), - 1 => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_CLIENT_DISCONNECT), - 2 => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_USER_REQUESTED), - 3 => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_SLOW_CONSUMER), - 4 => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_PROTOCOL_ERROR), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "EXEC_CANCEL_REASON_UNSPECIFIED" => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_UNSPECIFIED), - "EXEC_CANCEL_REASON_CLIENT_DISCONNECT" => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_CLIENT_DISCONNECT), - "EXEC_CANCEL_REASON_USER_REQUESTED" => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_USER_REQUESTED), - "EXEC_CANCEL_REASON_SLOW_CONSUMER" => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_SLOW_CONSUMER), - "EXEC_CANCEL_REASON_PROTOCOL_ERROR" => ::std::option::Option::Some(ExecCancelReason::EXEC_CANCEL_REASON_PROTOCOL_ERROR), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [ExecCancelReason] = &[ - ExecCancelReason::EXEC_CANCEL_REASON_UNSPECIFIED, - ExecCancelReason::EXEC_CANCEL_REASON_CLIENT_DISCONNECT, - ExecCancelReason::EXEC_CANCEL_REASON_USER_REQUESTED, - ExecCancelReason::EXEC_CANCEL_REASON_SLOW_CONSUMER, - ExecCancelReason::EXEC_CANCEL_REASON_PROTOCOL_ERROR, - ]; -} - -impl ::protobuf::EnumFull for ExecCancelReason { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("ExecCancelReason").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for ExecCancelReason { - fn default() -> Self { - ExecCancelReason::EXEC_CANCEL_REASON_UNSPECIFIED - } -} - -impl ExecCancelReason { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("ExecCancelReason") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.GuestControlErrorKind) -pub enum GuestControlErrorKind { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_UNSPECIFIED) - GUEST_CONTROL_ERROR_KIND_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_PROTOCOL_ERROR) - GUEST_CONTROL_ERROR_KIND_PROTOCOL_ERROR = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_MAX_CHUNK_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_MAX_CHUNK_EXCEEDED = 2, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE) - GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE = 3, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED) - GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED = 4, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN) - GUEST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN = 5, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED_BY_PROCESS) - GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED_BY_PROCESS = 6, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_STDIN_OFFSET_MISMATCH) - GUEST_CONTROL_ERROR_KIND_STDIN_OFFSET_MISMATCH = 7, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BUDGET_EXHAUSTED) - GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BUDGET_EXHAUSTED = 8, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED) - GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED = 9, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE) - GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE = 10, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED) - GUEST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED = 11, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_OUTPUT_LOST) - GUEST_CONTROL_ERROR_KIND_OUTPUT_LOST = 12, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UNAVAILABLE) - GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UNAVAILABLE = 13, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED) - GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED = 14, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED = 15, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED = 16, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_EXEC_NOT_FOUND) - GUEST_CONTROL_ERROR_KIND_EXEC_NOT_FOUND = 17, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_EXEC_ALREADY_EXITED) - GUEST_CONTROL_ERROR_KIND_EXEC_ALREADY_EXITED = 18, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_DISABLED) - GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_DISABLED = 19, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED) - GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED = 20, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED) - GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED = 21, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_CWD_INVALID) - GUEST_CONTROL_ERROR_KIND_CWD_INVALID = 22, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_CWD_DENIED) - GUEST_CONTROL_ERROR_KIND_CWD_DENIED = 23, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_PATH_UNSAFE) - GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_PATH_UNSAFE = 24, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QUOTA_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QUOTA_EXCEEDED = 25, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_EXCEEDED = 26, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED = 27, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT) - GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT = 28, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_RATE_LIMITED) - GUEST_CONTROL_ERROR_KIND_RATE_LIMITED = 29, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_REQUEST_ID_CONFLICT) - GUEST_CONTROL_ERROR_KIND_REQUEST_ID_CONFLICT = 30, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_CONTROL_SEQ_MISMATCH) - GUEST_CONTROL_ERROR_KIND_CONTROL_SEQ_MISMATCH = 31, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SLOW_CONSUMER_CANCELLED) - GUEST_CONTROL_ERROR_KIND_SLOW_CONSUMER_CANCELLED = 32, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_STALE_SESSION) - GUEST_CONTROL_ERROR_KIND_STALE_SESSION = 33, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATION) - GUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATION = 34, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_AUTH_FAILED) - GUEST_CONTROL_ERROR_KIND_AUTH_FAILED = 35, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE) - GUEST_CONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE = 36, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED) - GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED = 37, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND) - GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND = 38, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE) - GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE = 39, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_PATH_UNSAFE) - GUEST_CONTROL_ERROR_KIND_PATH_UNSAFE = 40, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_READ_DENIED) - GUEST_CONTROL_ERROR_KIND_READ_DENIED = 41, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM) - GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM = 42, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE) - GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE = 43, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED) - GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED = 44, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_BUS_ID) - GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_BUS_ID = 45, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST) - GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST = 46, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED) - GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED = 47, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME) - GUEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME = 48, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_CAPACITY_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_SHELL_CAPACITY_EXCEEDED = 49, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_ATTACH_CAPACITY_EXCEEDED) - GUEST_CONTROL_ERROR_KIND_SHELL_ATTACH_CAPACITY_EXCEEDED = 50, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_NOT_FOUND) - GUEST_CONTROL_ERROR_KIND_SHELL_NOT_FOUND = 51, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED) - GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED = 52, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE) - GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE = 53, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH) - GUEST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH = 54, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_SHELL_OUTPUT_GAP) - GUEST_CONTROL_ERROR_KIND_SHELL_OUTPUT_GAP = 55, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT) - GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT = 56, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT) - GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT = 57, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID) - GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID = 58, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH) - GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH = 59, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE) - GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE = 60, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_ACTIVATION_NOT_FOUND) - GUEST_CONTROL_ERROR_KIND_ACTIVATION_NOT_FOUND = 61, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_ACTIVATION_STATUS_UNAVAILABLE) - GUEST_CONTROL_ERROR_KIND_ACTIVATION_STATUS_UNAVAILABLE = 62, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT) - GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT = 63, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED) - GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED = 64, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE) - GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE = 65, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN) - GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN = 66, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE) - GUEST_CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE = 67, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.GuestControlErrorKind.GUEST_CONTROL_ERROR_KIND_AUDIO_ENFORCEMENT_FAILED) - GUEST_CONTROL_ERROR_KIND_AUDIO_ENFORCEMENT_FAILED = 68, -} - -impl ::protobuf::Enum for GuestControlErrorKind { - const NAME: &'static str = "GuestControlErrorKind"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED), - 1 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_PROTOCOL_ERROR), - 2 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_MAX_CHUNK_EXCEEDED), - 3 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE), - 4 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED), - 5 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN), - 6 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED_BY_PROCESS), - 7 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_OFFSET_MISMATCH), - 8 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BUDGET_EXHAUSTED), - 9 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED), - 10 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE), - 11 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED), - 12 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OUTPUT_LOST), - 13 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UNAVAILABLE), - 14 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED), - 15 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED), - 16 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED), - 17 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_NOT_FOUND), - 18 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_ALREADY_EXITED), - 19 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_DISABLED), - 20 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED), - 21 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED), - 22 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CWD_INVALID), - 23 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CWD_DENIED), - 24 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_PATH_UNSAFE), - 25 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QUOTA_EXCEEDED), - 26 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_EXCEEDED), - 27 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED), - 28 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT), - 29 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RATE_LIMITED), - 30 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_REQUEST_ID_CONFLICT), - 31 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CONTROL_SEQ_MISMATCH), - 32 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SLOW_CONSUMER_CANCELLED), - 33 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STALE_SESSION), - 34 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATION), - 35 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUTH_FAILED), - 36 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE), - 37 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED), - 38 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND), - 39 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE), - 40 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_PATH_UNSAFE), - 41 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_READ_DENIED), - 42 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM), - 43 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE), - 44 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED), - 45 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_BUS_ID), - 46 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST), - 47 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED), - 48 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME), - 49 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_CAPACITY_EXCEEDED), - 50 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_ATTACH_CAPACITY_EXCEEDED), - 51 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_NOT_FOUND), - 52 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED), - 53 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE), - 54 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH), - 55 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_OUTPUT_GAP), - 56 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT), - 57 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT), - 58 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID), - 59 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH), - 60 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE), - 61 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_NOT_FOUND), - 62 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_STATUS_UNAVAILABLE), - 63 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT), - 64 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED), - 65 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE), - 66 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN), - 67 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE), - 68 => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_ENFORCEMENT_FAILED), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "GUEST_CONTROL_ERROR_KIND_UNSPECIFIED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED), - "GUEST_CONTROL_ERROR_KIND_PROTOCOL_ERROR" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_PROTOCOL_ERROR), - "GUEST_CONTROL_ERROR_KIND_MAX_CHUNK_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_MAX_CHUNK_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE), - "GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED), - "GUEST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN), - "GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED_BY_PROCESS" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED_BY_PROCESS), - "GUEST_CONTROL_ERROR_KIND_STDIN_OFFSET_MISMATCH" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_OFFSET_MISMATCH), - "GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BUDGET_EXHAUSTED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BUDGET_EXHAUSTED), - "GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED), - "GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE), - "GUEST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED), - "GUEST_CONTROL_ERROR_KIND_OUTPUT_LOST" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OUTPUT_LOST), - "GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UNAVAILABLE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UNAVAILABLE), - "GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED), - "GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_EXEC_NOT_FOUND" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_NOT_FOUND), - "GUEST_CONTROL_ERROR_KIND_EXEC_ALREADY_EXITED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_ALREADY_EXITED), - "GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_DISABLED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_DISABLED), - "GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED), - "GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED), - "GUEST_CONTROL_ERROR_KIND_CWD_INVALID" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CWD_INVALID), - "GUEST_CONTROL_ERROR_KIND_CWD_DENIED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CWD_DENIED), - "GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_PATH_UNSAFE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_PATH_UNSAFE), - "GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QUOTA_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QUOTA_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT), - "GUEST_CONTROL_ERROR_KIND_RATE_LIMITED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RATE_LIMITED), - "GUEST_CONTROL_ERROR_KIND_REQUEST_ID_CONFLICT" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_REQUEST_ID_CONFLICT), - "GUEST_CONTROL_ERROR_KIND_CONTROL_SEQ_MISMATCH" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CONTROL_SEQ_MISMATCH), - "GUEST_CONTROL_ERROR_KIND_SLOW_CONSUMER_CANCELLED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SLOW_CONSUMER_CANCELLED), - "GUEST_CONTROL_ERROR_KIND_STALE_SESSION" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STALE_SESSION), - "GUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATION" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATION), - "GUEST_CONTROL_ERROR_KIND_AUTH_FAILED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUTH_FAILED), - "GUEST_CONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE), - "GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED), - "GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND), - "GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE), - "GUEST_CONTROL_ERROR_KIND_PATH_UNSAFE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_PATH_UNSAFE), - "GUEST_CONTROL_ERROR_KIND_READ_DENIED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_READ_DENIED), - "GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM), - "GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE), - "GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED), - "GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_BUS_ID" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_BUS_ID), - "GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST), - "GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED), - "GUEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME), - "GUEST_CONTROL_ERROR_KIND_SHELL_CAPACITY_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_CAPACITY_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_SHELL_ATTACH_CAPACITY_EXCEEDED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_ATTACH_CAPACITY_EXCEEDED), - "GUEST_CONTROL_ERROR_KIND_SHELL_NOT_FOUND" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_NOT_FOUND), - "GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED), - "GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE), - "GUEST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH), - "GUEST_CONTROL_ERROR_KIND_SHELL_OUTPUT_GAP" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_OUTPUT_GAP), - "GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT), - "GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT), - "GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID), - "GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH), - "GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE), - "GUEST_CONTROL_ERROR_KIND_ACTIVATION_NOT_FOUND" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_NOT_FOUND), - "GUEST_CONTROL_ERROR_KIND_ACTIVATION_STATUS_UNAVAILABLE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_STATUS_UNAVAILABLE), - "GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT), - "GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED), - "GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE), - "GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN), - "GUEST_CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE), - "GUEST_CONTROL_ERROR_KIND_AUDIO_ENFORCEMENT_FAILED" => ::std::option::Option::Some(GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_ENFORCEMENT_FAILED), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [GuestControlErrorKind] = &[ - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_PROTOCOL_ERROR, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_MAX_CHUNK_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED_BY_PROCESS, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_OFFSET_MISMATCH, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BUDGET_EXHAUSTED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_OUTPUT_LOST, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UNAVAILABLE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_NOT_FOUND, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_ALREADY_EXITED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_DISABLED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CWD_INVALID, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CWD_DENIED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_PATH_UNSAFE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QUOTA_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_RATE_LIMITED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_REQUEST_ID_CONFLICT, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_CONTROL_SEQ_MISMATCH, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SLOW_CONSUMER_CANCELLED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_STALE_SESSION, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATION, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUTH_FAILED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_PATH_UNSAFE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_READ_DENIED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_BUS_ID, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_CAPACITY_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_ATTACH_CAPACITY_EXCEEDED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_NOT_FOUND, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_SHELL_OUTPUT_GAP, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_NOT_FOUND, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_STATUS_UNAVAILABLE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE, - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_AUDIO_ENFORCEMENT_FAILED, - ]; -} - -impl ::protobuf::EnumFull for GuestControlErrorKind { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("GuestControlErrorKind").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for GuestControlErrorKind { - fn default() -> Self { - GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_UNSPECIFIED - } -} - -impl GuestControlErrorKind { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("GuestControlErrorKind") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.AudioChannel) -pub enum AudioChannel { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.AudioChannel.AUDIO_CHANNEL_UNSPECIFIED) - AUDIO_CHANNEL_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.AudioChannel.AUDIO_CHANNEL_MICROPHONE) - AUDIO_CHANNEL_MICROPHONE = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.AudioChannel.AUDIO_CHANNEL_SPEAKER) - AUDIO_CHANNEL_SPEAKER = 2, -} - -impl ::protobuf::Enum for AudioChannel { - const NAME: &'static str = "AudioChannel"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(AudioChannel::AUDIO_CHANNEL_UNSPECIFIED), - 1 => ::std::option::Option::Some(AudioChannel::AUDIO_CHANNEL_MICROPHONE), - 2 => ::std::option::Option::Some(AudioChannel::AUDIO_CHANNEL_SPEAKER), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "AUDIO_CHANNEL_UNSPECIFIED" => ::std::option::Option::Some(AudioChannel::AUDIO_CHANNEL_UNSPECIFIED), - "AUDIO_CHANNEL_MICROPHONE" => ::std::option::Option::Some(AudioChannel::AUDIO_CHANNEL_MICROPHONE), - "AUDIO_CHANNEL_SPEAKER" => ::std::option::Option::Some(AudioChannel::AUDIO_CHANNEL_SPEAKER), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [AudioChannel] = &[ - AudioChannel::AUDIO_CHANNEL_UNSPECIFIED, - AudioChannel::AUDIO_CHANNEL_MICROPHONE, - AudioChannel::AUDIO_CHANNEL_SPEAKER, - ]; -} - -impl ::protobuf::EnumFull for AudioChannel { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("AudioChannel").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for AudioChannel { - fn default() -> Self { - AudioChannel::AUDIO_CHANNEL_UNSPECIFIED - } -} - -impl AudioChannel { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("AudioChannel") - } -} - -#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] -// @@protoc_insertion_point(enum:d2b.guest.v1.AudioSetKind) -pub enum AudioSetKind { - // @@protoc_insertion_point(enum_value:d2b.guest.v1.AudioSetKind.AUDIO_SET_KIND_UNSPECIFIED) - AUDIO_SET_KIND_UNSPECIFIED = 0, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.AudioSetKind.AUDIO_SET_KIND_GRANT) - AUDIO_SET_KIND_GRANT = 1, - // @@protoc_insertion_point(enum_value:d2b.guest.v1.AudioSetKind.AUDIO_SET_KIND_LEVEL) - AUDIO_SET_KIND_LEVEL = 2, -} - -impl ::protobuf::Enum for AudioSetKind { - const NAME: &'static str = "AudioSetKind"; - - fn value(&self) -> i32 { - *self as i32 - } - - fn from_i32(value: i32) -> ::std::option::Option { - match value { - 0 => ::std::option::Option::Some(AudioSetKind::AUDIO_SET_KIND_UNSPECIFIED), - 1 => ::std::option::Option::Some(AudioSetKind::AUDIO_SET_KIND_GRANT), - 2 => ::std::option::Option::Some(AudioSetKind::AUDIO_SET_KIND_LEVEL), - _ => ::std::option::Option::None - } - } - - fn from_str(str: &str) -> ::std::option::Option { - match str { - "AUDIO_SET_KIND_UNSPECIFIED" => ::std::option::Option::Some(AudioSetKind::AUDIO_SET_KIND_UNSPECIFIED), - "AUDIO_SET_KIND_GRANT" => ::std::option::Option::Some(AudioSetKind::AUDIO_SET_KIND_GRANT), - "AUDIO_SET_KIND_LEVEL" => ::std::option::Option::Some(AudioSetKind::AUDIO_SET_KIND_LEVEL), - _ => ::std::option::Option::None - } - } - - const VALUES: &'static [AudioSetKind] = &[ - AudioSetKind::AUDIO_SET_KIND_UNSPECIFIED, - AudioSetKind::AUDIO_SET_KIND_GRANT, - AudioSetKind::AUDIO_SET_KIND_LEVEL, - ]; -} - -impl ::protobuf::EnumFull for AudioSetKind { - fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { - static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); - descriptor.get(|| file_descriptor().enum_by_package_relative_name("AudioSetKind").unwrap()).clone() - } - - fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { - let index = *self as usize; - Self::enum_descriptor().value_by_index(index) - } -} - -impl ::std::default::Default for AudioSetKind { - fn default() -> Self { - AudioSetKind::AUDIO_SET_KIND_UNSPECIFIED - } -} - -impl AudioSetKind { - fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { - ::protobuf::reflect::GeneratedEnumDescriptorData::new::("AudioSetKind") - } -} - -static file_descriptor_proto_data: &'static [u8] = b"\ - \n\x13guest_control.proto\x12\x0cd2b.guest.v1\"p\n\x0fRequestMetadata\ - \x12\x13\n\x05vm_id\x18\x01\x20\x01(\tR\x04vmId\x12\x1d\n\nrequest_id\ - \x18\x02\x20\x01(\tR\trequestId\x12)\n\x10protocol_version\x18\x03\x20\ - \x01(\rR\x0fprotocolVersion\"\x89\x01\n\x13ExecRequestMetadata\x125\n\ - \x06common\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x06co\ - mmon\x12\x17\n\x07exec_id\x18\x02\x20\x01(\tR\x06execId\x12\"\n\rguest_b\ - oot_id\x18\x03\x20\x01(\tR\x0bguestBootId\"\xc3\x01\n\x17TerminalRequest\ - Metadata\x125\n\x06common\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.Request\ - MetadataR\x06common\x12\x1d\n\nsession_id\x18\x02\x20\x01(\tR\tsessionId\ - \x12\"\n\rguest_boot_id\x18\x03\x20\x01(\tR\x0bguestBootId\x12.\n\x04kin\ - d\x18\x04\x20\x01(\x0e2\x1a.d2b.guest.v1.TerminalKindR\x04kind\"\x97\x01\ - \n\x0cHelloRequest\x129\n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.gues\ - t.v1.RequestMetadataR\x08metadata\x12\x1d\n\nhost_nonce\x18\x02\x20\x01(\ - \x0cR\thostNonce\x12-\n\x12transcript_version\x18\x03\x20\x01(\rR\x11tra\ - nscriptVersion\"\xa6\x01\n\rHelloResponse\x12\x1f\n\x0bguest_nonce\x18\ - \x01\x20\x01(\x0cR\nguestNonce\x12\"\n\rguest_boot_id\x18\x02\x20\x01(\t\ - R\x0bguestBootId\x12)\n\x10protocol_version\x18\x03\x20\x01(\rR\x0fproto\ - colVersionJ\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06R\x11capabilities_ha\ - shR\x06health\"\x87\x02\n\x13AuthenticateRequest\x129\n\x08metadata\x18\ - \x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metadata\x12\x1d\ - \n\nhost_nonce\x18\x02\x20\x01(\x0cR\thostNonce\x12\x1f\n\x0bguest_nonce\ - \x18\x03\x20\x01(\x0cR\nguestNonce\x12\"\n\rguest_boot_id\x18\x04\x20\ - \x01(\tR\x0bguestBootId\x12-\n\x12transcript_version\x18\x05\x20\x01(\rR\ - \x11transcriptVersion\x12\"\n\rhost_auth_tag\x18\x06\x20\x01(\x0cR\x0bho\ - stAuthTag\"\xd1\x02\n\x14AuthenticateResponse\x12)\n\x0eguest_auth_tag\ - \x18\x01\x20\x01(\x0cH\0R\x0cguestAuthTag\x88\x01\x01\x120\n\x11capabili\ - ties_hash\x18\x02\x20\x01(\tH\x01R\x10capabilitiesHash\x88\x01\x01\x124\ - \n\x06health\x18\x03\x20\x01(\x0b2\x1c.d2b.guest.v1.HealthResponseR\x06h\ - ealth\x12F\n\x0ccapabilities\x18\x04\x20\x01(\x0b2\".d2b.guest.v1.Capabi\ - litiesResponseR\x0ccapabilities\x125\n\x05error\x18\x05\x20\x01(\x0b2\ - \x1f.d2b.guest.v1.GuestControlErrorR\x05errorB\x11\n\x0f_guest_auth_tagB\ - \x14\n\x12_capabilities_hash\"P\n\x13CapabilitiesRequest\x129\n\x08metad\ - ata\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metadata\ - \"\xc0\x01\n\x14CapabilitiesResponse\x12)\n\x10protocol_version\x18\x01\ - \x20\x01(\rR\x0fprotocolVersion\x12A\n\x0ccapabilities\x18\x02\x20\x03(\ - \x0e2\x1d.d2b.guest.v1.GuestCapabilityR\x0ccapabilities\x12:\n\x06limits\ - \x18\x03\x20\x01(\x0b2\".d2b.guest.v1.GuestEffectiveLimitsR\x06limits\"J\ - \n\rHealthRequest\x129\n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.guest\ - .v1.RequestMetadataR\x08metadata\"\xa9\x03\n\x0eHealthResponse\x122\n\ - \x06origin\x18\x01\x20\x01(\x0e2\x1a.d2b.guest.v1.HealthOriginR\x06origi\ - n\x12/\n\x05state\x18\x02\x20\x01(\x0e2\x19.d2b.guest.v1.HealthStateR\ - \x05state\x122\n\x06reason\x18\x03\x20\x01(\x0e2\x1a.d2b.guest.v1.Health\ - ReasonR\x06reason\x12A\n\x0bremediation\x18\x04\x20\x01(\x0e2\x1f.d2b.gu\ - est.v1.HealthRemediationR\x0bremediation\x12)\n\x10protocol_version\x18\ - \x05\x20\x01(\rR\x0fprotocolVersion\x12A\n\x0ccapabilities\x18\x06\x20\ - \x03(\x0e2\x1d.d2b.guest.v1.GuestCapabilityR\x0ccapabilities\x12M\n\x13d\ - egraded_subsystems\x18\x07\x20\x03(\x0e2\x1c.d2b.guest.v1.GuestSubsystem\ - R\x12degradedSubsystems\"\x8b\t\n\x14GuestEffectiveLimits\x12&\n\x0fmax_\ - chunk_bytes\x18\x01\x20\x01(\x04R\rmaxChunkBytes\x123\n\x16max_recv_mess\ - age_bytes\x18\x02\x20\x01(\x04R\x13maxRecvMessageBytes\x12U\n(decoded_wr\ - ite_stdin_bytes_per_connection\x18\x03\x20\x01(\x04R#decodedWriteStdinBy\ - tesPerConnection\x12L\n#write_stdin_handlers_per_connection\x18\x04\x20\ - \x01(\rR\x1fwriteStdinHandlersPerConnection\x12<\n\x1bstdin_queue_chunks\ - _per_exec\x18\x05\x20\x01(\rR\x17stdinQueueChunksPerExec\x127\n\x18stdou\ - t_live_buffer_bytes\x18\x06\x20\x01(\x04R\x15stdoutLiveBufferBytes\x127\ - \n\x18stderr_live_buffer_bytes\x18\x07\x20\x01(\x04R\x15stderrLiveBuffer\ - Bytes\x129\n\x19detached_stdout_log_bytes\x18\x08\x20\x01(\x04R\x16detac\ - hedStdoutLogBytes\x129\n\x19detached_stderr_log_bytes\x18\t\x20\x01(\x04\ - R\x16detachedStderrLogBytes\x12/\n\x14long_poll_timeout_ms\x18\n\x20\x01\ - (\x04R\x11longPollTimeoutMs\x123\n\x16slow_consumer_grace_ms\x18\x0b\x20\ - \x01(\x04R\x13slowConsumerGraceMs\x12/\n\x14exec_sessions_per_vm\x18\x0c\ - \x20\x01(\rR\x11execSessionsPerVm\x127\n\x18attached_sessions_per_vm\x18\ - \r\x20\x01(\rR\x15attachedSessionsPerVm\x12M\n$pending_read_output_waits\ - _per_stream\x18\x0e\x20\x01(\rR\x1fpendingReadOutputWaitsPerStream\x128\ - \n\x19pending_exec_waits_per_vm\x18\x0f\x20\x01(\rR\x15pendingExecWaitsP\ - erVm\x12I\n\"rpc_rate_per_connection_per_second\x18\x10\x20\x01(\rR\x1dr\ - pcRatePerConnectionPerSecond\x120\n\x15rpc_rate_per_vm_burst\x18\x11\x20\ - \x01(\rR\x11rpcRatePerVmBurst\x121\n\x15shell_sessions_per_vm\x18\x12\ - \x20\x01(\rR\x12shellSessionsPerVm\x12B\n\x1eshell_attached_sessions_per\ - _vm\x18\x13\x20\x01(\rR\x1ashellAttachedSessionsPerVm\"\xa9\x03\n\x11Exe\ - cCreateRequest\x129\n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1\ - .RequestMetadataR\x08metadata\x12\x12\n\x04argv\x18\x02\x20\x03(\tR\x04a\ - rgv\x12\x17\n\x04user\x18\x03\x20\x01(\tH\0R\x04user\x88\x01\x01\x12\x15\ - \n\x03cwd\x18\x04\x20\x01(\tH\x01R\x03cwd\x88\x01\x01\x12&\n\x03env\x18\ - \x05\x20\x03(\x0b2\x14.d2b.guest.v1.EnvVarR\x03env\x12\x10\n\x03tty\x18\ - \x06\x20\x01(\x08R\x03tty\x12\x1d\n\nstdin_open\x18\x07\x20\x01(\x08R\ts\ - tdinOpen\x12\x1a\n\x08detached\x18\x08\x20\x01(\x08R\x08detached\x12N\n\ - \x15initial_terminal_size\x18\t\x20\x01(\x0b2\x1a.d2b.guest.v1.TerminalS\ - izeR\x13initialTerminalSize\x12?\n\routput_policy\x18\n\x20\x01(\x0b2\ - \x1a.d2b.guest.v1.OutputPolicyR\x0coutputPolicyB\x07\n\x05_userB\x06\n\ - \x04_cwd\"0\n\x06EnvVar\x12\x10\n\x03key\x18\x01\x20\x01(\tR\x03key\x12\ - \x14\n\x05value\x18\x02\x20\x01(\tR\x05value\"6\n\x0cTerminalSize\x12\ - \x12\n\x04rows\x18\x01\x20\x01(\rR\x04rows\x12\x12\n\x04cols\x18\x02\x20\ - \x01(\rR\x04cols\"\xf9\x01\n\x0cOutputPolicy\x12&\n\x0fmax_chunk_bytes\ - \x18\x01\x20\x01(\x04R\rmaxChunkBytes\x12/\n\x14max_stdout_log_bytes\x18\ - \x02\x20\x01(\x04R\x11maxStdoutLogBytes\x12/\n\x14max_stderr_log_bytes\ - \x18\x03\x20\x01(\x04R\x11maxStderrLogBytes\x127\n\x18slow_consumer_time\ - out_ms\x18\x04\x20\x01(\x04R\x15slowConsumerTimeoutMs\x12&\n\x0fwait_tim\ - eout_ms\x18\x05\x20\x01(\x04R\rwaitTimeoutMs\"\x95\x03\n\x12ExecCreateRe\ - sponse\x12\x1c\n\x07exec_id\x18\x01\x20\x01(\tH\0R\x06execId\x88\x01\x01\ - \x125\n\x17created_at_monotonic_ns\x18\x02\x20\x01(\x04R\x14createdAtMon\ - otonicNs\x12\x1f\n\x0bcontrol_seq\x18\x03\x20\x01(\x04R\ncontrolSeq\x12#\ - \n\rstdout_cursor\x18\x04\x20\x01(\x04R\x0cstdoutCursor\x12#\n\rstderr_c\ - ursor\x18\x05\x20\x01(\x04R\x0cstderrCursor\x12M\n\x10effective_limits\ - \x18\x06\x20\x01(\x0b2\".d2b.guest.v1.GuestEffectiveLimitsR\x0feffective\ - Limits\x12-\n\x05state\x18\x07\x20\x01(\x0e2\x17.d2b.guest.v1.ExecStateR\ - \x05state\x125\n\x05error\x18\x08\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestCo\ - ntrolErrorR\x05errorB\n\n\x08_exec_id\"S\n\x12ExecInspectRequest\x12=\n\ - \x08metadata\x18\x01\x20\x01(\x0b2!.d2b.guest.v1.ExecRequestMetadataR\ - \x08metadata\"\x87\x06\n\x13ExecInspectResponse\x12-\n\x05state\x18\x01\ - \x20\x01(\x0e2\x17.d2b.guest.v1.ExecStateR\x05state\x12T\n\x17visible_te\ - rminal_status\x18\x02\x20\x01(\x0b2\x1c.d2b.guest.v1.TerminalStatusR\x15\ - visibleTerminalStatus\x129\n\x0bstdin_state\x18\x03\x20\x01(\x0e2\x18.d2\ - b.guest.v1.StdinStateR\nstdinState\x12.\n\x13stdout_start_offset\x18\x04\ - \x20\x01(\x04R\x11stdoutStartOffset\x12*\n\x11stdout_end_offset\x18\x05\ - \x20\x01(\x04R\x0fstdoutEndOffset\x12.\n\x13stderr_start_offset\x18\x06\ - \x20\x01(\x04R\x11stderrStartOffset\x12*\n\x11stderr_end_offset\x18\x07\ - \x20\x01(\x04R\x0fstderrEndOffset\x120\n\x14stdout_dropped_bytes\x18\x08\ - \x20\x01(\x04R\x12stdoutDroppedBytes\x120\n\x14stderr_dropped_bytes\x18\ - \t\x20\x01(\x04R\x12stderrDroppedBytes\x12C\n\x1estdout_truncated_for_re\ - tention\x18\n\x20\x01(\x08R\x1bstdoutTruncatedForRetention\x12C\n\x1estd\ - err_truncated_for_retention\x18\x0b\x20\x01(\x08R\x1bstderrTruncatedForR\ - etention\x12(\n\x10last_control_seq\x18\x0c\x20\x01(\x04R\x0elastControl\ - Seq\x12)\n\x10state_generation\x18\r\x20\x01(\x04R\x0fstateGeneration\ - \x125\n\x05error\x18\x0e\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestControlErro\ - rR\x05error\"\xc5\x01\n\x0fExecWaitRequest\x12=\n\x08metadata\x18\x01\ - \x20\x01(\x0b2!.d2b.guest.v1.ExecRequestMetadataR\x08metadata\x12\x1d\n\ - \ntimeout_ms\x18\x02\x20\x01(\x04R\ttimeoutMs\x129\n\x16known_state_gene\ - ration\x18\x03\x20\x01(\x04H\0R\x14knownStateGeneration\x88\x01\x01B\x19\ - \n\x17_known_state_generation\"\xce\x03\n\x10ExecWaitResponse\x12-\n\x05\ - state\x18\x01\x20\x01(\x0e2\x17.d2b.guest.v1.ExecStateR\x05state\x12T\n\ - \x17visible_terminal_status\x18\x02\x20\x01(\x0b2\x1c.d2b.guest.v1.Termi\ - nalStatusR\x15visibleTerminalStatus\x12)\n\x10state_generation\x18\x03\ - \x20\x01(\x04R\x0fstateGeneration\x12.\n\x13stdout_start_offset\x18\x04\ - \x20\x01(\x04R\x11stdoutStartOffset\x12*\n\x11stdout_end_offset\x18\x05\ - \x20\x01(\x04R\x0fstdoutEndOffset\x12.\n\x13stderr_start_offset\x18\x06\ - \x20\x01(\x04R\x11stderrStartOffset\x12*\n\x11stderr_end_offset\x18\x07\ - \x20\x01(\x04R\x0fstderrEndOffset\x12\x1b\n\ttimed_out\x18\x08\x20\x01(\ - \x08R\x08timedOut\x125\n\x05error\x18\t\x20\x01(\x0b2\x1f.d2b.guest.v1.G\ - uestControlErrorR\x05error\"\xb5\x01\n\x0fExecLogsRequest\x12=\n\x08meta\ - data\x18\x01\x20\x01(\x0b2!.d2b.guest.v1.ExecRequestMetadataR\x08metadat\ - a\x122\n\x06stream\x18\x02\x20\x01(\x0e2\x1a.d2b.guest.v1.OutputStreamR\ - \x06stream\x12\x16\n\x06offset\x18\x03\x20\x01(\x04R\x06offset\x12\x17\n\ - \x07max_len\x18\x04\x20\x01(\x04R\x06maxLen\"\xe1\x02\n\x10ExecLogsRespo\ - nse\x122\n\x06stream\x18\x01\x20\x01(\x0e2\x1a.d2b.guest.v1.OutputStream\ - R\x06stream\x12\x16\n\x06offset\x18\x02\x20\x01(\x04R\x06offset\x12\x1d\ - \n\nend_offset\x18\x03\x20\x01(\x04R\tendOffset\x12\x12\n\x04data\x18\ - \x04\x20\x01(\x0cR\x04data\x12\x1f\n\x0bnext_offset\x18\x05\x20\x01(\x04\ - R\nnextOffset\x12\x10\n\x03eof\x18\x06\x20\x01(\x08R\x03eof\x12!\n\x0cst\ - art_offset\x18\x07\x20\x01(\x04R\x0bstartOffset\x12#\n\rdropped_bytes\ - \x18\x08\x20\x01(\x04R\x0cdroppedBytes\x12\x1c\n\ttruncated\x18\t\x20\ - \x01(\x08R\ttruncated\x125\n\x05error\x18\n\x20\x01(\x0b2\x1f.d2b.guest.\ - v1.GuestControlErrorR\x05error\"p\n\x0fExecListRequest\x129\n\x08metadat\ - a\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metadata\ - \x12\"\n\rguest_boot_id\x18\x02\x20\x01(\tR\x0bguestBootId\"\x80\x01\n\ - \x10ExecListResponse\x125\n\x07entries\x18\x01\x20\x03(\x0b2\x1b.d2b.gue\ - st.v1.ExecListEntryR\x07entries\x125\n\x05error\x18\x02\x20\x01(\x0b2\ - \x1f.d2b.guest.v1.GuestControlErrorR\x05error\"\xb1\x02\n\rExecListEntry\ - \x12\x17\n\x07exec_id\x18\x01\x20\x01(\tR\x06execId\x12\x12\n\x04slot\ - \x18\x02\x20\x01(\rR\x04slot\x12-\n\x05state\x18\x03\x20\x01(\x0e2\x17.d\ - 2b.guest.v1.ExecStateR\x05state\x12(\n\x10create_time_unix\x18\x04\x20\ - \x01(\x04R\x0ecreateTimeUnix\x12\x1f\n\x0bargv_sha256\x18\x05\x20\x01(\t\ - R\nargvSha256\x12)\n\x10stdout_truncated\x18\x06\x20\x01(\x08R\x0fstdout\ - Truncated\x12)\n\x10stderr_truncated\x18\x07\x20\x01(\x08R\x0fstderrTrun\ - cated\x12#\n\rdropped_bytes\x18\x08\x20\x01(\x04R\x0cdroppedBytes\"\xe9\ - \x01\n\x11WriteStdinRequest\x12=\n\x08metadata\x18\x01\x20\x01(\x0b2!.d2\ - b.guest.v1.ExecRequestMetadataR\x08metadata\x12\x16\n\x06offset\x18\x02\ - \x20\x01(\x04R\x06offset\x12\x12\n\x04data\x18\x03\x20\x01(\x0cR\x04data\ - \x12\x1f\n\x0bclose_after\x18\x04\x20\x01(\x08R\ncloseAfter\x121\n\x12cl\ - ient_deadline_ms\x18\x05\x20\x01(\x04H\0R\x10clientDeadlineMs\x88\x01\ - \x01B\x15\n\x13_client_deadline_ms\"\xd4\x02\n\x12WriteStdinResponse\x12\ - '\n\x0faccepted_offset\x18\x01\x20\x01(\x04R\x0eacceptedOffset\x12!\n\ - \x0caccepted_len\x18\x02\x20\x01(\x04R\x0bacceptedLen\x12\x1f\n\x0bnext_\ - offset\x18\x03\x20\x01(\x04R\nnextOffset\x129\n\x0bstdin_state\x18\x04\ - \x20\x01(\x0e2\x18.d2b.guest.v1.StdinStateR\nstdinState\x12\x1d\n\nblock\ - ed_ms\x18\x05\x20\x01(\x04R\tblockedMs\x12@\n\x0bdisposition\x18\x06\x20\ - \x01(\x0e2\x1e.d2b.guest.v1.WriteDispositionR\x0bdisposition\x125\n\x05e\ - rror\x18\x07\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestControlErrorR\x05error\ - \"\xea\x01\n\x11ReadOutputRequest\x12=\n\x08metadata\x18\x01\x20\x01(\ - \x0b2!.d2b.guest.v1.ExecRequestMetadataR\x08metadata\x122\n\x06stream\ - \x18\x02\x20\x01(\x0e2\x1a.d2b.guest.v1.OutputStreamR\x06stream\x12\x16\ - \n\x06offset\x18\x03\x20\x01(\x04R\x06offset\x12\x17\n\x07max_len\x18\ - \x04\x20\x01(\x04R\x06maxLen\x12\x12\n\x04wait\x18\x05\x20\x01(\x08R\x04\ - wait\x12\x1d\n\ntimeout_ms\x18\x06\x20\x01(\x04R\ttimeoutMs\"\x80\x03\n\ - \x12ReadOutputResponse\x122\n\x06stream\x18\x01\x20\x01(\x0e2\x1a.d2b.gu\ - est.v1.OutputStreamR\x06stream\x12\x16\n\x06offset\x18\x02\x20\x01(\x04R\ - \x06offset\x12\x1d\n\nend_offset\x18\x03\x20\x01(\x04R\tendOffset\x12\ - \x12\n\x04data\x18\x04\x20\x01(\x0cR\x04data\x12\x1f\n\x0bnext_offset\ - \x18\x05\x20\x01(\x04R\nnextOffset\x12\x10\n\x03eof\x18\x06\x20\x01(\x08\ - R\x03eof\x12!\n\x0cstart_offset\x18\x07\x20\x01(\x04R\x0bstartOffset\x12\ - #\n\rdropped_bytes\x18\x08\x20\x01(\x04R\x0cdroppedBytes\x12\x1c\n\ttrun\ - cated\x18\t\x20\x01(\x08R\ttruncated\x12\x1b\n\ttimed_out\x18\n\x20\x01(\ - \x08R\x08timedOut\x125\n\x05error\x18\x0b\x20\x01(\x0b2\x1f.d2b.guest.v1\ - .GuestControlErrorR\x05error\"j\n\x11CloseStdinRequest\x12=\n\x08metadat\ - a\x18\x01\x20\x01(\x0b2!.d2b.guest.v1.ExecRequestMetadataR\x08metadata\ - \x12\x16\n\x06offset\x18\x02\x20\x01(\x04R\x06offset\"\xeb\x01\n\x12Clos\ - eStdinResponse\x129\n\x0bstdin_state\x18\x01\x20\x01(\x0e2\x18.d2b.guest\ - .v1.StdinStateR\nstdinState\x12!\n\x0cfinal_offset\x18\x02\x20\x01(\x04R\ - \x0bfinalOffset\x12@\n\x0bdisposition\x18\x03\x20\x01(\x0e2\x1e.d2b.gues\ - t.v1.WriteDispositionR\x0bdisposition\x125\n\x05error\x18\x04\x20\x01(\ - \x0b2\x1f.d2b.guest.v1.GuestControlErrorR\x05error\"\x9d\x01\n\x13TtyWin\ - ResizeRequest\x12=\n\x08metadata\x18\x01\x20\x01(\x0b2!.d2b.guest.v1.Exe\ - cRequestMetadataR\x08metadata\x12\x1f\n\x0bcontrol_seq\x18\x02\x20\x01(\ - \x04R\ncontrolSeq\x12\x12\n\x04rows\x18\x03\x20\x01(\rR\x04rows\x12\x12\ - \n\x04cols\x18\x04\x20\x01(\rR\x04cols\"\xbf\x01\n\x11ExecSignalRequest\ - \x12=\n\x08metadata\x18\x01\x20\x01(\x0b2!.d2b.guest.v1.ExecRequestMetad\ - ataR\x08metadata\x12\x1f\n\x0bcontrol_seq\x18\x02\x20\x01(\x04R\ncontrol\ - Seq\x12\x16\n\x06signal\x18\x03\x20\x01(\rR\x06signal\x122\n\x06target\ - \x18\x04\x20\x01(\x0e2\x1a.d2b.guest.v1.SignalTargetR\x06target\"\xab\ - \x01\n\x11ExecCancelRequest\x12=\n\x08metadata\x18\x01\x20\x01(\x0b2!.d2\ - b.guest.v1.ExecRequestMetadataR\x08metadata\x12\x1f\n\x0bcontrol_seq\x18\ - \x02\x20\x01(\x04R\ncontrolSeq\x126\n\x06reason\x18\x03\x20\x01(\x0e2\ - \x1e.d2b.guest.v1.ExecCancelReasonR\x06reason\"\x82\x01\n\nControlAck\ - \x12\x1f\n\x0bcontrol_seq\x18\x01\x20\x01(\x04R\ncontrolSeq\x12\x1c\n\td\ - uplicate\x18\x02\x20\x01(\x08R\tduplicate\x125\n\x05error\x18\x03\x20\ - \x01(\x0b2\x1f.d2b.guest.v1.GuestControlErrorR\x05error\"\x85\x01\n\x14R\ - eadGuestFileRequest\x129\n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.gue\ - st.v1.RequestMetadataR\x08metadata\x122\n\x07file_id\x18\x02\x20\x01(\ - \x0e2\x19.d2b.guest.v1.GuestFileIdR\x06fileId\"\xd3\x01\n\x15ReadGuestFi\ - leResponse\x122\n\x07file_id\x18\x01\x20\x01(\x0e2\x19.d2b.guest.v1.Gues\ - tFileIdR\x06fileId\x12\x1d\n\nsize_bytes\x18\x02\x20\x01(\x04R\tsizeByte\ - s\x12\x18\n\x07content\x18\x03\x20\x01(\x0cR\x07content\x12\x16\n\x06sha\ - 256\x18\x04\x20\x01(\tR\x06sha256\x125\n\x05error\x18\x05\x20\x01(\x0b2\ - \x1f.d2b.guest.v1.GuestControlErrorR\x05error\"\xb3\x01\n\x12UsbipImport\ - Request\x129\n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.Reques\ - tMetadataR\x08metadata\x127\n\x06action\x18\x02\x20\x01(\x0e2\x1f.d2b.gu\ - est.v1.UsbipImportActionR\x06action\x12\x12\n\x04host\x18\x03\x20\x01(\t\ - R\x04host\x12\x15\n\x06bus_id\x18\x04\x20\x01(\tR\x05busId\"\xc3\x01\n\ - \x13UsbipImportResponse\x127\n\x06action\x18\x01\x20\x01(\x0e2\x1f.d2b.g\ - uest.v1.UsbipImportActionR\x06action\x12\x15\n\x06bus_id\x18\x02\x20\x01\ - (\tR\x05busId\x12%\n\x0edetached_ports\x18\x03\x20\x01(\rR\rdetachedPort\ - s\x125\n\x05error\x18\x04\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestControlErr\ - orR\x05error\"\x98\x01\n\x12UsbipStatusRequest\x129\n\x08metadata\x18\ - \x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metadata\x12\x17\ - \n\x04host\x18\x02\x20\x01(\tH\0R\x04host\x88\x01\x01\x12\x1a\n\x06bus_i\ - d\x18\x03\x20\x01(\tH\x01R\x05busId\x88\x01\x01B\x07\n\x05_hostB\t\n\x07\ - _bus_id\"\x86\x01\n\x13UsbipStatusResponse\x128\n\x07imports\x18\x01\x20\ - \x03(\x0b2\x1e.d2b.guest.v1.UsbipStatusEntryR\x07imports\x125\n\x05error\ - \x18\x02\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestControlErrorR\x05error\"l\n\ - \x10UsbipStatusEntry\x12\x12\n\x04port\x18\x01\x20\x01(\rR\x04port\x12\ - \x12\n\x04host\x18\x02\x20\x01(\tR\x04host\x12\x19\n\x08tcp_port\x18\x03\ - \x20\x01(\rR\x07tcpPort\x12\x15\n\x06bus_id\x18\x04\x20\x01(\tR\x05busId\ - \"\x81\x02\n\x1bGuestActivationStartRequest\x129\n\x08metadata\x18\x01\ - \x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metadata\x12#\n\ract\ - ivation_id\x18\x02\x20\x01(\tR\x0cactivationId\x12,\n\x12switch_script_p\ - ath\x18\x03\x20\x01(\tR\x10switchScriptPath\x125\n\x04mode\x18\x04\x20\ - \x01(\x0e2!.d2b.guest.v1.GuestActivationModeR\x04mode\x12\x1d\n\ntimeout\ - _ms\x18\x05\x20\x01(\x04R\ttimeoutMs\"\xb4\x01\n\x1cGuestActivationStart\ - Response\x12#\n\ractivation_id\x18\x01\x20\x01(\tR\x0cactivationId\x128\ - \n\x05state\x18\x02\x20\x01(\x0e2\".d2b.guest.v1.GuestActivationStateR\ - \x05state\x125\n\x05error\x18\x03\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestCo\ - ntrolErrorR\x05error\"~\n\x1cGuestActivationStatusRequest\x129\n\x08meta\ - data\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metadata\ - \x12#\n\ractivation_id\x18\x02\x20\x01(\tR\x0cactivationId\"\xc3\x02\n\ - \x1dGuestActivationStatusResponse\x12#\n\ractivation_id\x18\x01\x20\x01(\ - \tR\x0cactivationId\x128\n\x05state\x18\x02\x20\x01(\x0e2\".d2b.guest.v1\ - .GuestActivationStateR\x05state\x12\x20\n\texit_code\x18\x03\x20\x01(\ - \x05H\0R\x08exitCode\x88\x01\x01\x12\x1b\n\x06signal\x18\x04\x20\x01(\rH\ - \x01R\x06signal\x88\x01\x01\x12$\n\x0bstatus_code\x18\x05\x20\x01(\x05H\ - \x02R\nstatusCode\x88\x01\x01\x125\n\x05error\x18\x06\x20\x01(\x0b2\x1f.\ - d2b.guest.v1.GuestControlErrorR\x05errorB\x0c\n\n_exit_codeB\t\n\x07_sig\ - nalB\x0e\n\x0c_status_code\"\xd7\x01\n\x12ShellAttachRequest\x129\n\x08m\ - etadata\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metad\ - ata\x12\x17\n\x04name\x18\x02\x20\x01(\tH\0R\x04name\x88\x01\x01\x12\x14\ - \n\x05force\x18\x03\x20\x01(\x08R\x05force\x12N\n\x15initial_terminal_si\ - ze\x18\x04\x20\x01(\x0b2\x1a.d2b.guest.v1.TerminalSizeR\x13initialTermin\ - alSizeB\x07\n\x05_name\"\x8e\x03\n\x13ShellAttachResponse\x12\"\n\nsessi\ - on_id\x18\x01\x20\x01(\tH\0R\tsessionId\x88\x01\x01\x12#\n\rresolved_nam\ - e\x18\x02\x20\x01(\tR\x0cresolvedName\x12.\n\x05state\x18\x03\x20\x01(\ - \x0e2\x18.d2b.guest.v1.ShellStateR\x05state\x12#\n\rforce_evicted\x18\ - \x04\x20\x01(\x08R\x0cforceEvicted\x12\x1f\n\x0bcontrol_seq\x18\x05\x20\ - \x01(\x04R\ncontrolSeq\x12#\n\routput_cursor\x18\x06\x20\x01(\x04R\x0cou\ - tputCursor\x12M\n\x10effective_limits\x18\x07\x20\x01(\x0b2\".d2b.guest.\ - v1.GuestEffectiveLimitsR\x0feffectiveLimits\x125\n\x05error\x18\x08\x20\ - \x01(\x0b2\x1f.d2b.guest.v1.GuestControlErrorR\x05errorB\r\n\x0b_session\ - _id\"M\n\x10ShellListRequest\x129\n\x08metadata\x18\x01\x20\x01(\x0b2\ - \x1d.d2b.guest.v1.RequestMetadataR\x08metadata\"\xa7\x01\n\x11ShellListR\ - esponse\x12!\n\x0cdefault_name\x18\x01\x20\x01(\tR\x0bdefaultName\x128\n\ - \x08sessions\x18\x02\x20\x03(\x0b2\x1c.d2b.guest.v1.ShellListEntryR\x08s\ - essions\x125\n\x05error\x18\x03\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestCont\ - rolErrorR\x05error\"\x8f\x01\n\x0eShellListEntry\x12\x12\n\x04name\x18\ - \x01\x20\x01(\tR\x04name\x12.\n\x05state\x18\x02\x20\x01(\x0e2\x18.d2b.g\ - uest.v1.ShellStateR\x05state\x12\x1a\n\x08attached\x18\x03\x20\x01(\x08R\ - \x08attached\x12\x1d\n\nis_default\x18\x04\x20\x01(\x08R\tisDefault\"q\n\ - \x12ShellDetachRequest\x129\n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.\ - guest.v1.RequestMetadataR\x08metadata\x12\x17\n\x04name\x18\x02\x20\x01(\ - \tH\0R\x04name\x88\x01\x01B\x07\n\x05_name\"a\n\x10ShellKillRequest\x129\ - \n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\ - \x08metadata\x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\"\\\n\x17Shel\ - lCloseAttachRequest\x12A\n\x08metadata\x18\x01\x20\x01(\x0b2%.d2b.guest.\ - v1.TerminalRequestMetadataR\x08metadata\"\xc2\x01\n\x13ShellDetachRespon\ - se\x12#\n\rresolved_name\x18\x01\x20\x01(\tR\x0cresolvedName\x12\x1a\n\ - \x08detached\x18\x02\x20\x01(\x08R\x08detached\x123\n\x05cause\x18\x03\ - \x20\x01(\x0e2\x1d.d2b.guest.v1.ShellCloseCauseR\x05cause\x125\n\x05erro\ - r\x18\x04\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestControlErrorR\x05error\"\ - \xa6\x01\n\x11ShellKillResponse\x12\x12\n\x04name\x18\x01\x20\x01(\tR\ - \x04name\x12\x16\n\x06killed\x18\x02\x20\x01(\x08R\x06killed\x12.\n\x05s\ - tate\x18\x03\x20\x01(\x0e2\x18.d2b.guest.v1.ShellStateR\x05state\x125\n\ - \x05error\x18\x04\x20\x01(\x0b2\x1f.d2b.guest.v1.GuestControlErrorR\x05e\ - rror\"\xf5\x01\n\x19TerminalWriteStdinRequest\x12A\n\x08metadata\x18\x01\ - \x20\x01(\x0b2%.d2b.guest.v1.TerminalRequestMetadataR\x08metadata\x12\ - \x16\n\x06offset\x18\x02\x20\x01(\x04R\x06offset\x12\x12\n\x04data\x18\ - \x03\x20\x01(\x0cR\x04data\x12\x1f\n\x0bclose_after\x18\x04\x20\x01(\x08\ - R\ncloseAfter\x121\n\x12client_deadline_ms\x18\x05\x20\x01(\x04H\0R\x10c\ - lientDeadlineMs\x88\x01\x01B\x15\n\x13_client_deadline_ms\"\xf6\x01\n\ - \x19TerminalReadOutputRequest\x12A\n\x08metadata\x18\x01\x20\x01(\x0b2%.\ - d2b.guest.v1.TerminalRequestMetadataR\x08metadata\x122\n\x06stream\x18\ - \x02\x20\x01(\x0e2\x1a.d2b.guest.v1.OutputStreamR\x06stream\x12\x16\n\ - \x06offset\x18\x03\x20\x01(\x04R\x06offset\x12\x17\n\x07max_len\x18\x04\ - \x20\x01(\x04R\x06maxLen\x12\x12\n\x04wait\x18\x05\x20\x01(\x08R\x04wait\ - \x12\x1d\n\ntimeout_ms\x18\x06\x20\x01(\x04R\ttimeoutMs\"v\n\x19Terminal\ - CloseStdinRequest\x12A\n\x08metadata\x18\x01\x20\x01(\x0b2%.d2b.guest.v1\ - .TerminalRequestMetadataR\x08metadata\x12\x16\n\x06offset\x18\x02\x20\ - \x01(\x04R\x06offset\"\xa9\x01\n\x1bTerminalTtyWinResizeRequest\x12A\n\ - \x08metadata\x18\x01\x20\x01(\x0b2%.d2b.guest.v1.TerminalRequestMetadata\ - R\x08metadata\x12\x1f\n\x0bcontrol_seq\x18\x02\x20\x01(\x04R\ncontrolSeq\ - \x12\x12\n\x04rows\x18\x03\x20\x01(\rR\x04rows\x12\x12\n\x04cols\x18\x04\ - \x20\x01(\rR\x04cols\"\xb4\x01\n\x0eTerminalStatus\x12\x1d\n\texit_code\ - \x18\x01\x20\x01(\x05H\0R\x08exitCode\x12\x18\n\x06signal\x18\x02\x20\ - \x01(\rH\0R\x06signal\x12!\n\x0bstatus_code\x18\x03\x20\x01(\x05H\0R\nst\ - atusCode\x12;\n\x05error\x18\x04\x20\x01(\x0e2#.d2b.guest.v1.GuestContro\ - lErrorKindH\0R\x05errorB\t\n\x07outcome\"\xcd\x01\n\x11GuestControlError\ - \x127\n\x04kind\x18\x01\x20\x01(\x0e2#.d2b.guest.v1.GuestControlErrorKin\ - dR\x04kind\x12A\n\x0bremediation\x18\x02\x20\x01(\x0e2\x1f.d2b.guest.v1.\ - HealthRemediationR\x0bremediation\x12)\n\x0eretry_after_ms\x18\x03\x20\ - \x01(\x04H\0R\x0cretryAfterMs\x88\x01\x01B\x11\n\x0f_retry_after_ms\"e\n\ - \x16GuestAudioChannelState\x12\x14\n\x05muted\x18\x01\x20\x01(\x08R\x05m\ - uted\x12\x14\n\x05level\x18\x02\x20\x01(\rR\x05level\x12\x1f\n\x0blevel_\ - known\x18\x03\x20\x01(\x08R\nlevelKnown\"O\n\x12AudioStatusRequest\x129\ - \n\x08metadata\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\ - \x08metadata\"\xd2\x01\n\x13AudioStatusResponse\x12D\n\nmicrophone\x18\ - \x01\x20\x01(\x0b2$.d2b.guest.v1.GuestAudioChannelStateR\nmicrophone\x12\ - >\n\x07speaker\x18\x02\x20\x01(\x0b2$.d2b.guest.v1.GuestAudioChannelStat\ - eR\x07speaker\x125\n\x05error\x18\x03\x20\x01(\x0b2\x1f.d2b.guest.v1.Gue\ - stControlErrorR\x05error\"\xe3\x01\n\x0fAudioSetRequest\x129\n\x08metada\ - ta\x18\x01\x20\x01(\x0b2\x1d.d2b.guest.v1.RequestMetadataR\x08metadata\ - \x124\n\x07channel\x18\x02\x20\x01(\x0e2\x1a.d2b.guest.v1.AudioChannelR\ - \x07channel\x12.\n\x04kind\x18\x03\x20\x01(\x0e2\x1a.d2b.guest.v1.AudioS\ - etKindR\x04kind\x12\x19\n\x08grant_on\x18\x04\x20\x01(\x08R\x07grantOn\ - \x12\x14\n\x05level\x18\x05\x20\x01(\rR\x05level\"\x85\x01\n\x10AudioSet\ - Response\x12:\n\x05state\x18\x01\x20\x01(\x0b2$.d2b.guest.v1.GuestAudioC\ - hannelStateR\x05state\x125\n\x05error\x18\x02\x20\x01(\x0b2\x1f.d2b.gues\ - t.v1.GuestControlErrorR\x05error*\x85\x05\n\x0fGuestCapability\x12\x20\n\ - \x1cGUEST_CAPABILITY_UNSPECIFIED\x10\0\x12\x1b\n\x17GUEST_CAPABILITY_HEA\ - LTH\x10\x01\x12!\n\x1dGUEST_CAPABILITY_CAPABILITIES\x10\x02\x12\"\n\x1eG\ - UEST_CAPABILITY_EXEC_ATTACHED\x10\x03\x12\"\n\x1eGUEST_CAPABILITY_EXEC_D\ - ETACHED\x10\x04\x12\x1d\n\x19GUEST_CAPABILITY_EXEC_TTY\x10\x05\x12\x1e\n\ - \x1aGUEST_CAPABILITY_EXEC_LOGS\x10\x06\x12\x1f\n\x1bGUEST_CAPABILITY_TTY\ - _RESIZE\x10\x07\x12\x1c\n\x18GUEST_CAPABILITY_SIGNALS\x10\x08\x12$\n\x20\ - GUEST_CAPABILITY_READ_GUEST_FILE\x10\t\x12!\n\x1dGUEST_CAPABILITY_USBIP_\ - IMPORT\x10\n\x12#\n\x1fGUEST_CAPABILITY_SHELL_ATTACHED\x10\x0b\x12%\n!GU\ - EST_CAPABILITY_SHELL_MANAGEMENT\x10\x0c\x12'\n#GUEST_CAPABILITY_SHELL_FO\ - RCE_ATTACH\x10\r\x12!\n\x1dGUEST_CAPABILITY_USBIP_STATUS\x10\x0e\x12&\n\ - \"GUEST_CAPABILITY_SYSTEM_ACTIVATION\x10\x0f\x12!\n\x1dGUEST_CAPABILITY_\ - AUDIO_STATUS\x10\x10\x12\x1e\n\x1aGUEST_CAPABILITY_AUDIO_SET\x10\x11*\ - \xed\x02\n\x0eGuestSubsystem\x12\x1f\n\x1bGUEST_SUBSYSTEM_UNSPECIFIED\ - \x10\0\x12\x1a\n\x16GUEST_SUBSYSTEM_GUESTD\x10\x01\x12\x19\n\x15GUEST_SU\ - BSYSTEM_USERD\x10\x02\x12\x18\n\x14GUEST_SUBSYSTEM_EXEC\x10\x03\x12\x1f\ - \n\x1bGUEST_SUBSYSTEM_LOG_STORAGE\x10\x04\x12\x19\n\x15GUEST_SUBSYSTEM_T\ - OKEN\x10\x05\x12\x19\n\x15GUEST_SUBSYSTEM_VSOCK\x10\x06\x12\x19\n\x15GUE\ - ST_SUBSYSTEM_USBIP\x10\x07\x12\x19\n\x15GUEST_SUBSYSTEM_SHELL\x10\x08\ - \x12\x1a\n\x16GUEST_SUBSYSTEM_SHPOOL\x10\t\x12%\n!GUEST_SUBSYSTEM_SYSTEM\ - _ACTIVATION\x10\n\x12\x19\n\x15GUEST_SUBSYSTEM_AUDIO\x10\x0b*^\n\x0cTerm\ - inalKind\x12\x1d\n\x19TERMINAL_KIND_UNSPECIFIED\x10\0\x12\x16\n\x12TERMI\ - NAL_KIND_EXEC\x10\x01\x12\x17\n\x13TERMINAL_KIND_SHELL\x10\x02*\xc6\x01\ - \n\x13GuestActivationMode\x12%\n!GUEST_ACTIVATION_MODE_UNSPECIFIED\x10\0\ - \x12\x20\n\x1cGUEST_ACTIVATION_MODE_SWITCH\x10\x01\x12\x1e\n\x1aGUEST_AC\ - TIVATION_MODE_BOOT\x10\x02\x12\x1e\n\x1aGUEST_ACTIVATION_MODE_TEST\x10\ - \x03\x12&\n\"GUEST_ACTIVATION_MODE_DRY_ACTIVATE\x10\x04*\xf2\x01\n\x14Gu\ - estActivationState\x12&\n\"GUEST_ACTIVATION_STATE_UNSPECIFIED\x10\0\x12\ - \"\n\x1eGUEST_ACTIVATION_STATE_RUNNING\x10\x01\x12$\n\x20GUEST_ACTIVATIO\ - N_STATE_SUCCEEDED\x10\x02\x12!\n\x1dGUEST_ACTIVATION_STATE_FAILED\x10\ - \x03\x12$\n\x20GUEST_ACTIVATION_STATE_TIMED_OUT\x10\x04\x12\x1f\n\x1bGUE\ - ST_ACTIVATION_STATE_LOST\x10\x05*\xd5\x01\n\nShellState\x12\x1b\n\x17SHE\ - LL_STATE_UNSPECIFIED\x10\0\x12\x18\n\x14SHELL_STATE_ATTACHED\x10\x01\x12\ - \x18\n\x14SHELL_STATE_DETACHED\x10\x02\x12\x16\n\x12SHELL_STATE_KILLED\ - \x10\x03\x12\x20\n\x1cSHELL_STATE_POOL_UNAVAILABLE\x10\x04\x12\x20\n\x1c\ - SHELL_STATE_FEATURE_DISABLED\x10\x05\x12\x1a\n\x16SHELL_STATE_OUTPUT_GAP\ - \x10\x06*\xa1\x02\n\x0fShellCloseCause\x12!\n\x1dSHELL_CLOSE_CAUSE_UNSPE\ - CIFIED\x10\0\x12#\n\x1fSHELL_CLOSE_CAUSE_CLIENT_DETACH\x10\x01\x12&\n\"S\ - HELL_CLOSE_CAUSE_EVICTED_BY_FORCE\x10\x02\x12-\n)SHELL_CLOSE_CAUSE_EVICT\ - ED_BY_ADMIN_DETACH\x10\x03\x12%\n!SHELL_CLOSE_CAUSE_KILLED_BY_ADMIN\x10\ - \x04\x12&\n\"SHELL_CLOSE_CAUSE_POOL_UNAVAILABLE\x10\x05\x12\x20\n\x1cSHE\ - LL_CLOSE_CAUSE_OUTPUT_GAP\x10\x06*L\n\x0bGuestFileId\x12\x1d\n\x19GUEST_\ - FILE_ID_UNSPECIFIED\x10\0\x12\x1e\n\x1aGUEST_FILE_ID_GUEST_CONFIG\x10\ - \x01*x\n\x11UsbipImportAction\x12#\n\x1fUSBIP_IMPORT_ACTION_UNSPECIFIED\ - \x10\0\x12\x1e\n\x1aUSBIP_IMPORT_ACTION_ATTACH\x10\x01\x12\x1e\n\x1aUSBI\ - P_IMPORT_ACTION_DETACH\x10\x02*s\n\x0cHealthOrigin\x12\x1d\n\x19HEALTH_O\ - RIGIN_UNSPECIFIED\x10\0\x12\x20\n\x1cHEALTH_ORIGIN_GUEST_REPORTED\x10\ - \x01\x12\"\n\x1eHEALTH_ORIGIN_HOST_SYNTHESIZED\x10\x02*\xb9\x02\n\x0bHea\ - lthState\x12\x1c\n\x18HEALTH_STATE_UNSPECIFIED\x10\0\x12\x18\n\x14HEALTH\ - _STATE_HEALTHY\x10\x01\x12\x19\n\x15HEALTH_STATE_DEGRADED\x10\x02\x12+\n\ - 'HEALTH_STATE_UNAVAILABLE_OLD_GENERATION\x10\x03\x12\x20\n\x1cHEALTH_STA\ - TE_LISTENER_ABSENT\x10\x04\x12&\n\"HEALTH_STATE_TRANSPORT_UNREACHABLE\ - \x10\x05\x12\x1c\n\x18HEALTH_STATE_AUTH_FAILED\x10\x06\x12\"\n\x1eHEALTH\ - _STATE_PROTOCOL_MISMATCH\x10\x07\x12\x1e\n\x1aHEALTH_STATE_STALE_SESSION\ - \x10\x08*\xa4\x05\n\x0cHealthReason\x12\x1d\n\x19HEALTH_REASON_UNSPECIFI\ - ED\x10\0\x12\x16\n\x12HEALTH_REASON_NONE\x10\x01\x12\x20\n\x1cHEALTH_REA\ - SON_OLD_GENERATION\x10\x02\x12!\n\x1dHEALTH_REASON_LISTENER_ABSENT\x10\ - \x03\x12!\n\x1dHEALTH_REASON_CONNECT_REFUSED\x10\x04\x12!\n\x1dHEALTH_RE\ - ASON_CONNECT_TIMEOUT\x10\x05\x12\x20\n\x1cHEALTH_REASON_EOF_BEFORE_ACK\ - \x10\x06\x12\x1f\n\x1bHEALTH_REASON_MALFORMED_ACK\x10\x07\x12\x1e\n\x1aH\ - EALTH_REASON_ACK_TOO_LONG\x10\x08\x12\x1e\n\x1aHEALTH_REASON_TRANSPORT_I\ - O\x10\t\x12%\n!HEALTH_REASON_AUTH_TOKEN_REJECTED\x10\n\x12.\n*HEALTH_REA\ - SON_PROTOCOL_VERSION_UNSUPPORTED\x10\x0b\x12-\n)HEALTH_REASON_SESSION_GE\ - NERATION_MISMATCH\x10\x0c\x12,\n(HEALTH_REASON_EXEC_SUBSYSTEM_UNAVAILABL\ - E\x10\r\x12)\n%HEALTH_REASON_LOG_STORAGE_UNAVAILABLE\x10\x0e\x12\x20\n\ - \x1cHEALTH_REASON_QUOTA_EXCEEDED\x10\x0f\x12\x1e\n\x1aHEALTH_REASON_RATE\ - _LIMITED\x10\x10\x12.\n*HEALTH_REASON_INTERNAL_HEALTH_CHECK_FAILED\x10\ - \x11*\xe0\x02\n\x11HealthRemediation\x12\"\n\x1eHEALTH_REMEDIATION_UNSPE\ - CIFIED\x10\0\x12\x1b\n\x17HEALTH_REMEDIATION_NONE\x10\x01\x12\x1c\n\x18H\ - EALTH_REMEDIATION_RETRY\x10\x02\x12!\n\x1dHEALTH_REMEDIATION_RESTART_VM\ - \x10\x03\x12$\n\x20HEALTH_REMEDIATION_UPGRADE_GUEST\x10\x04\x12'\n#HEALT\ - H_REMEDIATION_CHECK_AUTH_TOKEN\x10\x05\x12+\n'HEALTH_REMEDIATION_CHECK_G\ - UESTD_SERVICE\x10\x06\x12\"\n\x1eHEALTH_REMEDIATION_REDUCE_LOAD\x10\x07\ - \x12)\n%HEALTH_REMEDIATION_INSPECT_GUEST_LOGS\x10\x08*a\n\x0cOutputStrea\ - m\x12\x1d\n\x19OUTPUT_STREAM_UNSPECIFIED\x10\0\x12\x18\n\x14OUTPUT_STREA\ - M_STDOUT\x10\x01\x12\x18\n\x14OUTPUT_STREAM_STDERR\x10\x02*\x96\x01\n\ - \x10WriteDisposition\x12!\n\x1dWRITE_DISPOSITION_UNSPECIFIED\x10\0\x12\ - \x1e\n\x1aWRITE_DISPOSITION_ACCEPTED\x10\x01\x12\x1f\n\x1bWRITE_DISPOSIT\ - ION_DUPLICATE\x10\x02\x12\x1e\n\x1aWRITE_DISPOSITION_REJECTED\x10\x03*\ - \x9b\x02\n\tExecState\x12\x1a\n\x16EXEC_STATE_UNSPECIFIED\x10\0\x12\x16\ - \n\x12EXEC_STATE_CREATED\x10\x01\x12\x16\n\x12EXEC_STATE_RUNNING\x10\x02\ - \x12\x15\n\x11EXEC_STATE_EXITED\x10\x03\x12\x17\n\x13EXEC_STATE_SIGNALED\ - \x10\x04\x12\x18\n\x14EXEC_STATE_CANCELLED\x10\x05\x12&\n\"EXEC_STATE_SL\ - OW_CONSUMER_CANCELLED\x10\x06\x12\x1d\n\x19EXEC_STATE_PROTOCOL_ERROR\x10\ - \x07\x12\x1a\n\x16EXEC_STATE_LOST_GUESTD\x10\x08\x12\x15\n\x11EXEC_STATE\ - _REAPED\x10\t*\xbd\x01\n\nStdinState\x12\x1b\n\x17STDIN_STATE_UNSPECIFIE\ - D\x10\0\x12\x14\n\x10STDIN_STATE_OPEN\x10\x01\x12\x17\n\x13STDIN_STATE_C\ - LOSING\x10\x02\x12\x16\n\x12STDIN_STATE_CLOSED\x10\x03\x12!\n\x1dSTDIN_S\ - TATE_CLOSED_BY_PROCESS\x10\x04\x12(\n$STDIN_STATE_REJECTED_NOT_INTERACTI\ - VE\x10\x05*y\n\x0cSignalTarget\x12\x1d\n\x19SIGNAL_TARGET_UNSPECIFIED\ - \x10\0\x12*\n&SIGNAL_TARGET_FOREGROUND_PROCESS_GROUP\x10\x01\x12\x1e\n\ - \x1aSIGNAL_TARGET_PROCESS_TREE\x10\x02*\xd4\x01\n\x10ExecCancelReason\ - \x12\"\n\x1eEXEC_CANCEL_REASON_UNSPECIFIED\x10\0\x12(\n$EXEC_CANCEL_REAS\ - ON_CLIENT_DISCONNECT\x10\x01\x12%\n!EXEC_CANCEL_REASON_USER_REQUESTED\ - \x10\x02\x12$\n\x20EXEC_CANCEL_REASON_SLOW_CONSUMER\x10\x03\x12%\n!EXEC_\ - CANCEL_REASON_PROTOCOL_ERROR\x10\x04*\xaa\x1b\n\x15GuestControlErrorKind\ - \x12(\n$GUEST_CONTROL_ERROR_KIND_UNSPECIFIED\x10\0\x12+\n'GUEST_CONTROL_\ - ERROR_KIND_PROTOCOL_ERROR\x10\x01\x12/\n+GUEST_CONTROL_ERROR_KIND_MAX_CH\ - UNK_EXCEEDED\x10\x02\x12/\n+GUEST_CONTROL_ERROR_KIND_STDIN_BACKPRESSURE\ - \x10\x03\x12)\n%GUEST_CONTROL_ERROR_KIND_STDIN_CLOSED\x10\x04\x12+\n'GUE\ - ST_CONTROL_ERROR_KIND_STDIN_NOT_OPEN\x10\x05\x124\n0GUEST_CONTROL_ERROR_\ - KIND_STDIN_CLOSED_BY_PROCESS\x10\x06\x122\n.GUEST_CONTROL_ERROR_KIND_STD\ - IN_OFFSET_MISMATCH\x10\x07\x128\n4GUEST_CONTROL_ERROR_KIND_STDIN_BYTE_BU\ - DGET_EXHAUSTED\x10\x08\x12+\n'GUEST_CONTROL_ERROR_KIND_OFFSET_EXPIRED\ - \x10\t\x12-\n)GUEST_CONTROL_ERROR_KIND_OFFSET_IN_FUTURE\x10\n\x12-\n)GUE\ - ST_CONTROL_ERROR_KIND_OFFSET_EXHAUSTED\x10\x0b\x12(\n$GUEST_CONTROL_ERRO\ - R_KIND_OUTPUT_LOST\x10\x0c\x123\n/GUEST_CONTROL_ERROR_KIND_TTY_STDERR_UN\ - AVAILABLE\x10\r\x12)\n%GUEST_CONTROL_ERROR_KIND_TTY_REQUIRED\x10\x0e\x12\ - 3\n/GUEST_CONTROL_ERROR_KIND_EXEC_CAPACITY_EXCEEDED\x10\x0f\x12:\n6GUEST\ - _CONTROL_ERROR_KIND_EXEC_ATTACH_CAPACITY_EXCEEDED\x10\x10\x12+\n'GUEST_C\ - ONTROL_ERROR_KIND_EXEC_NOT_FOUND\x10\x11\x120\n,GUEST_CONTROL_ERROR_KIND\ - _EXEC_ALREADY_EXITED\x10\x12\x120\n,GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_\ - DISABLED\x10\x13\x123\n/GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_ROOT_DENIED\ - \x10\x14\x123\n/GUEST_CONTROL_ERROR_KIND_GUEST_EXEC_USER_DENIED\x10\x15\ - \x12(\n$GUEST_CONTROL_ERROR_KIND_CWD_INVALID\x10\x16\x12'\n#GUEST_CONTRO\ - L_ERROR_KIND_CWD_DENIED\x10\x17\x125\n1GUEST_CONTROL_ERROR_KIND_RETAINED\ - _LOG_PATH_UNSAFE\x10\x18\x128\n4GUEST_CONTROL_ERROR_KIND_RETAINED_LOG_QU\ - OTA_EXCEEDED\x10\x19\x128\n4GUEST_CONTROL_ERROR_KIND_READ_WAIT_CAPACITY_\ - EXCEEDED\x10\x1a\x123\n/GUEST_CONTROL_ERROR_KIND_WAIT_CAPACITY_EXCEEDED\ - \x10\x1b\x121\n-GUEST_CONTROL_ERROR_KIND_SUPERSEDED_READ_WAIT\x10\x1c\ - \x12)\n%GUEST_CONTROL_ERROR_KIND_RATE_LIMITED\x10\x1d\x120\n,GUEST_CONTR\ - OL_ERROR_KIND_REQUEST_ID_CONFLICT\x10\x1e\x121\n-GUEST_CONTROL_ERROR_KIN\ - D_CONTROL_SEQ_MISMATCH\x10\x1f\x124\n0GUEST_CONTROL_ERROR_KIND_SLOW_CONS\ - UMER_CANCELLED\x10\x20\x12*\n&GUEST_CONTROL_ERROR_KIND_STALE_SESSION\x10\ - !\x12E\nAGUEST_CONTROL_ERROR_KIND_GUEST_CONTROL_UNAVAILABLE_OLD_GENERATI\ - ON\x10\"\x12(\n$GUEST_CONTROL_ERROR_KIND_AUTH_FAILED\x10#\x122\n.GUEST_C\ - ONTROL_ERROR_KIND_TRANSPORT_UNREACHABLE\x10$\x12)\n%GUEST_CONTROL_ERROR_\ - KIND_EXEC_EXPIRED\x10%\x12+\n'GUEST_CONTROL_ERROR_KIND_FILE_NOT_FOUND\ - \x10&\x12+\n'GUEST_CONTROL_ERROR_KIND_FILE_TOO_LARGE\x10'\x12(\n$GUEST_C\ - ONTROL_ERROR_KIND_PATH_UNSAFE\x10(\x12(\n$GUEST_CONTROL_ERROR_KIND_READ_\ - DENIED\x10)\x12,\n(GUEST_CONTROL_ERROR_KIND_INVALID_PROGRAM\x10*\x12.\n*\ - GUEST_CONTROL_ERROR_KIND_USBIP_UNAVAILABLE\x10+\x121\n-GUEST_CONTROL_ERR\ - OR_KIND_USBIP_COMMAND_FAILED\x10,\x121\n-GUEST_CONTROL_ERROR_KIND_USBIP_\ - INVALID_BUS_ID\x10-\x12/\n+GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST\ - \x10.\x121\n-GUEST_CONTROL_ERROR_KIND_GUEST_SHELL_DISABLED\x10/\x12/\n+G\ - UEST_CONTROL_ERROR_KIND_SHELL_INVALID_NAME\x100\x124\n0GUEST_CONTROL_ERR\ - OR_KIND_SHELL_CAPACITY_EXCEEDED\x101\x12;\n7GUEST_CONTROL_ERROR_KIND_SHE\ - LL_ATTACH_CAPACITY_EXCEEDED\x102\x12,\n(GUEST_CONTROL_ERROR_KIND_SHELL_N\ - OT_FOUND\x103\x123\n/GUEST_CONTROL_ERROR_KIND_SHELL_ALREADY_ATTACHED\x10\ - 4\x123\n/GUEST_CONTROL_ERROR_KIND_SHELL_POOL_UNAVAILABLE\x105\x128\n4GUE\ - ST_CONTROL_ERROR_KIND_SHELL_DAEMON_EPOCH_MISMATCH\x106\x12-\n)GUEST_CONT\ - ROL_ERROR_KIND_SHELL_OUTPUT_GAP\x107\x122\n.GUEST_CONTROL_ERROR_KIND_USB\ - IP_COMMAND_TIMEOUT\x108\x121\n-GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OU\ - TPUT\x109\x122\n.GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_ID\x10:\x12\ - 4\n0GUEST_CONTROL_ERROR_KIND_ACTIVATION_INVALID_PATH\x10;\x124\n0GUEST_C\ - ONTROL_ERROR_KIND_ACTIVATION_INVALID_MODE\x10<\x121\n-GUEST_CONTROL_ERRO\ - R_KIND_ACTIVATION_NOT_FOUND\x10=\x12:\n6GUEST_CONTROL_ERROR_KIND_ACTIVAT\ - ION_STATUS_UNAVAILABLE\x10>\x121\n-GUEST_CONTROL_ERROR_KIND_ACTIVATION_T\ - IMED_OUT\x10?\x124\n0GUEST_CONTROL_ERROR_KIND_ACTIVATION_SPAWN_FAILED\ - \x10@\x127\n3GUEST_CONTROL_ERROR_KIND_AUDIO_PIPEWIRE_UNAVAILABLE\x10A\ - \x122\n.GUEST_CONTROL_ERROR_KIND_AUDIO_CHANNEL_UNKNOWN\x10B\x125\n1GUEST\ - _CONTROL_ERROR_KIND_AUDIO_LEVEL_OUT_OF_RANGE\x10C\x125\n1GUEST_CONTROL_E\ - RROR_KIND_AUDIO_ENFORCEMENT_FAILED\x10D*f\n\x0cAudioChannel\x12\x1d\n\ - \x19AUDIO_CHANNEL_UNSPECIFIED\x10\0\x12\x1c\n\x18AUDIO_CHANNEL_MICROPHON\ - E\x10\x01\x12\x19\n\x15AUDIO_CHANNEL_SPEAKER\x10\x02*b\n\x0cAudioSetKind\ - \x12\x1e\n\x1aAUDIO_SET_KIND_UNSPECIFIED\x10\0\x12\x18\n\x14AUDIO_SET_KI\ - ND_GRANT\x10\x01\x12\x18\n\x14AUDIO_SET_KIND_LEVEL\x10\x02b\x06proto3\ -"; - -/// `FileDescriptorProto` object which was a source for this generated file -fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { - static file_descriptor_proto_lazy: ::protobuf::rt::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::Lazy::new(); - file_descriptor_proto_lazy.get(|| { - ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() - }) -} - -/// `FileDescriptor` object which allows dynamic access to files -pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { - static generated_file_descriptor_lazy: ::protobuf::rt::Lazy<::protobuf::reflect::GeneratedFileDescriptor> = ::protobuf::rt::Lazy::new(); - static file_descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::FileDescriptor> = ::protobuf::rt::Lazy::new(); - file_descriptor.get(|| { - let generated_file_descriptor = generated_file_descriptor_lazy.get(|| { - let mut deps = ::std::vec::Vec::with_capacity(0); - let mut messages = ::std::vec::Vec::with_capacity(68); - messages.push(RequestMetadata::generated_message_descriptor_data()); - messages.push(ExecRequestMetadata::generated_message_descriptor_data()); - messages.push(TerminalRequestMetadata::generated_message_descriptor_data()); - messages.push(HelloRequest::generated_message_descriptor_data()); - messages.push(HelloResponse::generated_message_descriptor_data()); - messages.push(AuthenticateRequest::generated_message_descriptor_data()); - messages.push(AuthenticateResponse::generated_message_descriptor_data()); - messages.push(CapabilitiesRequest::generated_message_descriptor_data()); - messages.push(CapabilitiesResponse::generated_message_descriptor_data()); - messages.push(HealthRequest::generated_message_descriptor_data()); - messages.push(HealthResponse::generated_message_descriptor_data()); - messages.push(GuestEffectiveLimits::generated_message_descriptor_data()); - messages.push(ExecCreateRequest::generated_message_descriptor_data()); - messages.push(EnvVar::generated_message_descriptor_data()); - messages.push(TerminalSize::generated_message_descriptor_data()); - messages.push(OutputPolicy::generated_message_descriptor_data()); - messages.push(ExecCreateResponse::generated_message_descriptor_data()); - messages.push(ExecInspectRequest::generated_message_descriptor_data()); - messages.push(ExecInspectResponse::generated_message_descriptor_data()); - messages.push(ExecWaitRequest::generated_message_descriptor_data()); - messages.push(ExecWaitResponse::generated_message_descriptor_data()); - messages.push(ExecLogsRequest::generated_message_descriptor_data()); - messages.push(ExecLogsResponse::generated_message_descriptor_data()); - messages.push(ExecListRequest::generated_message_descriptor_data()); - messages.push(ExecListResponse::generated_message_descriptor_data()); - messages.push(ExecListEntry::generated_message_descriptor_data()); - messages.push(WriteStdinRequest::generated_message_descriptor_data()); - messages.push(WriteStdinResponse::generated_message_descriptor_data()); - messages.push(ReadOutputRequest::generated_message_descriptor_data()); - messages.push(ReadOutputResponse::generated_message_descriptor_data()); - messages.push(CloseStdinRequest::generated_message_descriptor_data()); - messages.push(CloseStdinResponse::generated_message_descriptor_data()); - messages.push(TtyWinResizeRequest::generated_message_descriptor_data()); - messages.push(ExecSignalRequest::generated_message_descriptor_data()); - messages.push(ExecCancelRequest::generated_message_descriptor_data()); - messages.push(ControlAck::generated_message_descriptor_data()); - messages.push(ReadGuestFileRequest::generated_message_descriptor_data()); - messages.push(ReadGuestFileResponse::generated_message_descriptor_data()); - messages.push(UsbipImportRequest::generated_message_descriptor_data()); - messages.push(UsbipImportResponse::generated_message_descriptor_data()); - messages.push(UsbipStatusRequest::generated_message_descriptor_data()); - messages.push(UsbipStatusResponse::generated_message_descriptor_data()); - messages.push(UsbipStatusEntry::generated_message_descriptor_data()); - messages.push(GuestActivationStartRequest::generated_message_descriptor_data()); - messages.push(GuestActivationStartResponse::generated_message_descriptor_data()); - messages.push(GuestActivationStatusRequest::generated_message_descriptor_data()); - messages.push(GuestActivationStatusResponse::generated_message_descriptor_data()); - messages.push(ShellAttachRequest::generated_message_descriptor_data()); - messages.push(ShellAttachResponse::generated_message_descriptor_data()); - messages.push(ShellListRequest::generated_message_descriptor_data()); - messages.push(ShellListResponse::generated_message_descriptor_data()); - messages.push(ShellListEntry::generated_message_descriptor_data()); - messages.push(ShellDetachRequest::generated_message_descriptor_data()); - messages.push(ShellKillRequest::generated_message_descriptor_data()); - messages.push(ShellCloseAttachRequest::generated_message_descriptor_data()); - messages.push(ShellDetachResponse::generated_message_descriptor_data()); - messages.push(ShellKillResponse::generated_message_descriptor_data()); - messages.push(TerminalWriteStdinRequest::generated_message_descriptor_data()); - messages.push(TerminalReadOutputRequest::generated_message_descriptor_data()); - messages.push(TerminalCloseStdinRequest::generated_message_descriptor_data()); - messages.push(TerminalTtyWinResizeRequest::generated_message_descriptor_data()); - messages.push(TerminalStatus::generated_message_descriptor_data()); - messages.push(GuestControlError::generated_message_descriptor_data()); - messages.push(GuestAudioChannelState::generated_message_descriptor_data()); - messages.push(AudioStatusRequest::generated_message_descriptor_data()); - messages.push(AudioStatusResponse::generated_message_descriptor_data()); - messages.push(AudioSetRequest::generated_message_descriptor_data()); - messages.push(AudioSetResponse::generated_message_descriptor_data()); - let mut enums = ::std::vec::Vec::with_capacity(22); - enums.push(GuestCapability::generated_enum_descriptor_data()); - enums.push(GuestSubsystem::generated_enum_descriptor_data()); - enums.push(TerminalKind::generated_enum_descriptor_data()); - enums.push(GuestActivationMode::generated_enum_descriptor_data()); - enums.push(GuestActivationState::generated_enum_descriptor_data()); - enums.push(ShellState::generated_enum_descriptor_data()); - enums.push(ShellCloseCause::generated_enum_descriptor_data()); - enums.push(GuestFileId::generated_enum_descriptor_data()); - enums.push(UsbipImportAction::generated_enum_descriptor_data()); - enums.push(HealthOrigin::generated_enum_descriptor_data()); - enums.push(HealthState::generated_enum_descriptor_data()); - enums.push(HealthReason::generated_enum_descriptor_data()); - enums.push(HealthRemediation::generated_enum_descriptor_data()); - enums.push(OutputStream::generated_enum_descriptor_data()); - enums.push(WriteDisposition::generated_enum_descriptor_data()); - enums.push(ExecState::generated_enum_descriptor_data()); - enums.push(StdinState::generated_enum_descriptor_data()); - enums.push(SignalTarget::generated_enum_descriptor_data()); - enums.push(ExecCancelReason::generated_enum_descriptor_data()); - enums.push(GuestControlErrorKind::generated_enum_descriptor_data()); - enums.push(AudioChannel::generated_enum_descriptor_data()); - enums.push(AudioSetKind::generated_enum_descriptor_data()); - ::protobuf::reflect::GeneratedFileDescriptor::new_generated( - file_descriptor_proto(), - deps, - messages, - enums, - ) - }); - ::protobuf::reflect::FileDescriptor::new_generated_2(generated_file_descriptor) - }) -} diff --git a/packages/d2b-contracts-control/src/generated/mod.rs b/packages/d2b-contracts-control/src/generated/mod.rs deleted file mode 100644 index e9f10e00fb..0000000000 --- a/packages/d2b-contracts-control/src/generated/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -// @generated - -pub mod guest_control; diff --git a/packages/d2b-contracts-control/src/guest_auth.rs b/packages/d2b-contracts-control/src/guest_auth.rs deleted file mode 100644 index 25c16abdf5..0000000000 --- a/packages/d2b-contracts-control/src/guest_auth.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Guest-control authentication transcript helpers. -//! -//! This module is intentionally transport/runtime free so guestd, d2bd, -//! and the privileged broker can share one canonical HMAC transcript encoder -//! without pulling ttRPC into `d2b-contracts`. - -pub const AUTH_TRANSCRIPT_VERSION: u32 = 1; -pub use d2b_contracts::auth_wire::AUTH_NONCE_LEN; -pub const AUTH_TAG_LEN: usize = 32; -pub const GUEST_CONTROL_AUTH_PORT: u32 = 14_318; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum AuthDirection { - HostToGuest, -} - -impl AuthDirection { - pub const fn label(self) -> &'static [u8] { - match self { - Self::HostToGuest => b"host-to-guest", - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum AuthPurpose { - GuestControlAuthV1, -} - -impl AuthPurpose { - pub const fn label(self) -> &'static [u8] { - match self { - Self::GuestControlAuthV1 => b"guest-control-auth-v1", - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ProofRole { - Host, - Guest, -} - -impl ProofRole { - pub const fn label(self) -> &'static [u8] { - match self { - Self::Host => b"host-proof", - Self::Guest => b"guest-proof", - } - } -} - -pub struct GuestAuthTranscript<'a> { - pub role: ProofRole, - pub direction: AuthDirection, - pub purpose: AuthPurpose, - pub vm_id: &'a str, - pub protocol_version: u32, - pub guest_control_port: u32, - pub peer_cid: Option, - pub host_nonce: &'a [u8; AUTH_NONCE_LEN], - pub guest_nonce: &'a [u8; AUTH_NONCE_LEN], - pub guest_boot_id: &'a str, - pub capabilities_hash: Option<&'a [u8]>, -} - -pub fn encode_transcript(transcript: &GuestAuthTranscript<'_>) -> Vec { - let mut out = Vec::new(); - push_field(&mut out, 1, b"guest-control-auth-v1"); - push_field(&mut out, 2, transcript.role.label()); - push_field(&mut out, 3, transcript.direction.label()); - push_field(&mut out, 4, transcript.purpose.label()); - push_field(&mut out, 5, transcript.vm_id.as_bytes()); - push_field(&mut out, 6, &transcript.protocol_version.to_be_bytes()); - push_field(&mut out, 7, &transcript.guest_control_port.to_be_bytes()); - if let Some(peer_cid) = transcript.peer_cid { - push_field(&mut out, 8, &peer_cid.to_be_bytes()); - } - push_field(&mut out, 10, transcript.host_nonce); - push_field(&mut out, 11, transcript.guest_nonce); - push_field(&mut out, 12, transcript.guest_boot_id.as_bytes()); - if let Some(capabilities_hash) = transcript.capabilities_hash { - push_field(&mut out, 13, capabilities_hash); - } - out -} - -fn push_field(out: &mut Vec, tag: u8, value: &[u8]) { - out.push(tag); - out.extend_from_slice(&(value.len() as u32).to_be_bytes()); - out.extend_from_slice(value); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn transcript_encoding_is_stable() { - let host_nonce = [0x11; AUTH_NONCE_LEN]; - let guest_nonce = [0x22; AUTH_NONCE_LEN]; - let encoded = encode_transcript(&GuestAuthTranscript { - role: ProofRole::Host, - direction: AuthDirection::HostToGuest, - purpose: AuthPurpose::GuestControlAuthV1, - vm_id: "corp-vm", - protocol_version: 1, - guest_control_port: GUEST_CONTROL_AUTH_PORT, - peer_cid: Some(2), - host_nonce: &host_nonce, - guest_nonce: &guest_nonce, - guest_boot_id: "boot-1", - capabilities_hash: None, - }); - assert_eq!(&encoded[..5], &[1, 0, 0, 0, 21]); - assert_eq!(encoded[5..26].as_ref(), b"guest-control-auth-v1"); - assert!(encoded.windows(16).all(|window| window != [0x33; 16])); - } -} diff --git a/packages/d2b-contracts-control/src/guest_wire.rs b/packages/d2b-contracts-control/src/guest_wire.rs deleted file mode 100644 index 59662d6405..0000000000 --- a/packages/d2b-contracts-control/src/guest_wire.rs +++ /dev/null @@ -1,1752 +0,0 @@ -//! Guest-control ttRPC/protobuf schema DTOs. -//! -//! These Rust DTOs are the schema oracle for the guest-control protobuf -//! surface selected by ADR 0028. They intentionally model the message -//! contract, not a JSON transport: implementations generate protobuf/ttRPC -//! bindings from the matching `.proto` surface and keep these DTOs aligned -//! through the schema drift gate. - -use schemars::{ - JsonSchema, - r#gen::SchemaGenerator, - schema::{InstanceType, Schema, SchemaObject, SingleOrVec, StringValidation}, -}; -use serde::{Deserialize, Serialize}; - -pub const GUEST_CONTROL_SCHEMA_VERSION: &str = "v2"; -pub const GUEST_CONTROL_PROTOCOL_VERSION: u32 = 6; -pub const GUEST_CONTROL_VSOCK_PORT: u32 = 14_318; -pub const TTRPC_FRAME_CAP_BYTES: u64 = 4 * 1024 * 1024; -pub const DEFAULT_MAX_CHUNK_BYTES: u64 = 64 * 1024; -pub const HARD_MAX_CHUNK_BYTES: u64 = 1024 * 1024; - -/// Maximum raw byte size of a single-shot `ReadGuestFile` payload. -/// -/// Deliberately set BELOW both the public.sock 1 MiB frame -/// (`d2b_contracts::MAX_FRAME_SIZE`) and the ttRPC frame cap, with margin for the -/// base64 + JSON envelope a public.sock response adds: 512 KiB raw base64-encodes -/// to ~699 KiB, which still fits a 1 MiB frame with envelope overhead. A guest -/// file larger than this fails with `FileTooLarge` BEFORE any read/allocation. -pub const READ_GUEST_FILE_MAX_BYTES: u64 = 512 * 1024; - -/// Standard padded base64 expansion of `raw_len` bytes: `ceil(raw_len/3)*4`. -pub const fn base64_encoded_len(raw_len: usize) -> usize { - raw_len.div_ceil(3) * 4 -} - -/// Maximum base64-encoded length of a single-shot guest-config read payload, -/// DERIVED from [`READ_GUEST_FILE_MAX_BYTES`]. Used by the daemon's -/// `ReadGuestConfig` verb to fail closed (`FileTooLarge`) before it serializes -/// an oversize response. The const assertion below proves the encoded payload, -/// plus a JSON-envelope margin, fits under BOTH the public.sock frame -/// (`d2b_contracts::MAX_FRAME_SIZE`) and the ttRPC frame ([`TTRPC_FRAME_CAP_BYTES`]). -pub const READ_GUEST_CONFIG_ENCODED_MAX_BYTES: usize = - base64_encoded_len(READ_GUEST_FILE_MAX_BYTES as usize); - -/// Reserve for the JSON envelope wrapped around the base64 content in a -/// public.sock response frame (`{"kind":"...","payload":{"contentBase64":...}}`). -pub const READ_GUEST_CONFIG_ENVELOPE_MARGIN: usize = 8 * 1024; - -// Static proof that the derived encoded cap (plus envelope margin) fits within -// both transport frames. If a future cap bump breaks either framing, this fails -// to compile rather than overflowing a frame at runtime. -#[allow(clippy::assertions_on_constants)] -const _: () = { - assert!( - READ_GUEST_CONFIG_ENCODED_MAX_BYTES + READ_GUEST_CONFIG_ENVELOPE_MARGIN - < d2b_contracts::MAX_FRAME_SIZE, - "base64 guest-config payload + envelope must fit the public.sock frame" - ); - assert!( - (READ_GUEST_CONFIG_ENCODED_MAX_BYTES as u64) < TTRPC_FRAME_CAP_BYTES, - "base64 guest-config payload must fit the ttRPC frame" - ); -}; - -/// Fail-closed check that a base64-encoded guest-config payload fits within -/// BOTH transport frames. Returns `true` iff the encoded length is within the -/// derived cap (which is statically proven to fit the public.sock and ttRPC -/// frames). The daemon calls this on the encoded payload before responding. -pub fn guest_config_encoded_within_frame_caps(encoded_len: usize) -> bool { - encoded_len <= READ_GUEST_CONFIG_ENCODED_MAX_BYTES - && encoded_len + READ_GUEST_CONFIG_ENVELOPE_MARGIN < d2b_contracts::MAX_FRAME_SIZE - && (encoded_len as u64) < TTRPC_FRAME_CAP_BYTES -} - -macro_rules! bounded_string { - ($(#[$meta:meta])* $name:ident, $max:literal) => { - $(#[$meta])* - #[derive( - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - )] - #[serde(transparent)] - pub struct $name(pub String); - - impl $name { - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - } - - impl JsonSchema for $name { - fn schema_name() -> String { - stringify!($name).to_owned() - } - - fn json_schema( - _gen: &mut schemars::r#gen::SchemaGenerator, - ) -> schemars::schema::Schema { - schemars::schema::Schema::Object(schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new( - schemars::schema::InstanceType::String, - ))), - string: Some(Box::new(schemars::schema::StringValidation { - min_length: Some(1), - max_length: Some($max), - ..Default::default() - })), - ..Default::default() - }) - } - } - }; -} - -macro_rules! bounded_bytes { - ($(#[$meta:meta])* $name:ident, $min:literal, $max:literal) => { - $(#[$meta])* - #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] - #[serde(transparent)] - pub struct $name(pub Vec); - - impl JsonSchema for $name { - fn schema_name() -> String { - stringify!($name).to_owned() - } - - fn json_schema( - _gen: &mut schemars::r#gen::SchemaGenerator, - ) -> schemars::schema::Schema { - let item = schemars::schema::Schema::Object(schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new( - schemars::schema::InstanceType::Integer, - ))), - format: Some("uint8".to_owned()), - number: Some(Box::new(schemars::schema::NumberValidation { - minimum: Some(0.0), - maximum: Some(255.0), - ..Default::default() - })), - ..Default::default() - }); - - schemars::schema::Schema::Object(schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new( - schemars::schema::InstanceType::Array, - ))), - array: Some(Box::new(schemars::schema::ArrayValidation { - items: Some(schemars::schema::SingleOrVec::Single(Box::new(item))), - min_items: Some($min), - max_items: Some($max), - ..Default::default() - })), - ..Default::default() - }) - } - } - }; -} - -bounded_bytes! { - /// Stdin payload bytes. - GuestStdinBytes, 1, 1048576 -} - -bounded_bytes! { - /// Stdout/stderr payload bytes. - GuestOutputBytes, 0, 1048576 -} - -bounded_bytes! { - /// Single-shot `ReadGuestFile` content bytes. Capped at - /// `READ_GUEST_FILE_MAX_BYTES` (512 KiB) so the encoded response fits - /// inside both the public.sock and ttRPC frame caps. - GuestFileBytes, 0, 524288 -} - -bounded_string! { - /// Guest-control schema version token. - GuestSchemaVersion, 32 -} - -bounded_string! { - /// Cloud Hypervisor CONNECT request line shape. - GuestConnectRequestLine, 64 -} - -bounded_string! { - /// Cloud Hypervisor OK acknowledgement line shape. - GuestConnectAckLine, 64 -} - -bounded_string! { - /// VM identity bound into guest-control auth transcripts. - GuestVmId, 128 -} - -bounded_string! { - /// Guest-control request idempotency key. - RequestId, 128 -} - -bounded_string! { - /// Guest-control exec session id. - ExecId, 128 -} - -bounded_string! { - /// Guest-control terminal session id. - TerminalSessionId, 128 -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] -#[serde(transparent)] -pub struct ShellName(pub String); - -impl std::fmt::Debug for ShellName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("ShellName()") - } -} - -impl<'de> Deserialize<'de> for ShellName { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if shell_name_valid(&value) { - Ok(Self(value)) - } else { - Err(serde::de::Error::custom( - "shell name must match ^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$", - )) - } - } -} - -impl JsonSchema for ShellName { - fn schema_name() -> String { - "ShellName".to_owned() - } - - fn json_schema(_gen: &mut SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), - string: Some(Box::new(StringValidation { - min_length: Some(1), - max_length: Some(64), - pattern: Some("^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$".to_owned()), - })), - ..Default::default() - }) - } -} - -fn shell_name_valid(value: &str) -> bool { - let bytes = value.as_bytes(); - if bytes.is_empty() || bytes.len() > 64 { - return false; - } - let first = bytes[0]; - (first.is_ascii_alphanumeric() || first == b'_') - && bytes[1..] - .iter() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) -} - -bounded_string! { - /// Hex-encoded SHA-256 of a detached exec's argv (never the raw argv). - ArgvSha256, 64 -} - -bounded_bytes! { - /// Fixed-size raw challenge nonce bytes. - GuestNonce, 32, 32 -} - -bounded_bytes! { - /// Fixed-size HMAC-SHA256 proof tag bytes. - GuestAuthTag, 32, 32 -} - -bounded_string! { - /// Guest boot identity for stale-session detection. - GuestBootId, 128 -} - -bounded_string! { - /// Hash of the negotiated bounded capability set. - CapabilitiesHash, 128 -} - -bounded_string! { - /// One command argument. - GuestArg, 4096 -} - -bounded_string! { - /// Guest user selector. - GuestUser, 128 -} - -bounded_string! { - /// Guest working directory. - GuestCwd, 4096 -} - -bounded_string! { - /// Guest environment variable name. - EnvKey, 128 -} - -bounded_string! { - /// Guest environment variable value. - EnvValue, 8192 -} - -bounded_string! { - /// Guest-side USBIP backend host address. - GuestUsbipHost, 64 -} - -bounded_string! { - /// USB sysfs bus id (`B-P[.P...]`). - GuestUsbipBusId, 31 -} - -bounded_string! { - /// Host-generated activation id (UUID text). - GuestActivationId, 36 -} - -bounded_string! { - /// Guest /nix/store switch-to-configuration script path. - GuestSwitchScriptPath, 4096 -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestCapability { - Health, - Capabilities, - ExecAttached, - ExecDetached, - ExecTty, - ExecLogs, - TtyResize, - Signals, - ReadGuestFile, - UsbipImport, - ShellAttached, - ShellManagement, - ShellForceAttach, - UsbipStatus, - SystemActivation, - /// Read current audio policy state from the guest PipeWire session. - /// Advertised only when guestd is configured with a PipeWire control path. - AudioStatus, - /// Mutate mic/speaker grant or level in the guest PipeWire session. - /// Advertised alongside `AudioStatus`; requires the same configuration. - AudioSet, -} - -/// Closed set of host-declared guest files readable via `ReadGuestFile`. -/// Currently ships only `GuestConfig` (the in-guest editable config working copy). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestFileId { - GuestConfig, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestSubsystem { - Guestd, - Userd, - Exec, - LogStorage, - Token, - Vsock, - Usbip, - Shell, - Shpool, - SystemActivation, - Audio, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum TerminalKind { - Exec, - Shell, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum ShellState { - Attached, - Detached, - Killed, - PoolUnavailable, - FeatureDisabled, - OutputGap, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum ShellCloseCause { - ClientDetach, - EvictedByForce, - EvictedByAdminDetach, - KilledByAdmin, - PoolUnavailable, - OutputGap, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum HealthOrigin { - GuestReported, - HostSynthesized, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestVsockDirection { - HostToGuest, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestIdentityBinding { - VmIdCidPortAndTokenTranscript, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestControlSchema { - pub schema_version: GuestSchemaVersion, - pub protocol_version: u32, - pub transport: GuestTransportSchema, - pub limits: GuestEffectiveLimits, - pub hello: HelloRequest, - pub hello_ok: HelloResponse, - pub authenticate: AuthenticateRequest, - pub authenticated: AuthenticateResponse, - pub health_request: HealthRequest, - pub health: HealthResponse, - pub capabilities_request: CapabilitiesRequest, - pub capabilities: CapabilitiesResponse, - pub exec_create: ExecCreateRequest, - pub exec_created: ExecCreateResponse, - pub exec_inspect: ExecInspectRequest, - pub exec_inspected: ExecInspectResponse, - pub exec_wait: ExecWaitRequest, - pub exec_waited: ExecWaitResponse, - pub exec_logs: ExecLogsRequest, - pub exec_log_chunk: ExecLogsResponse, - pub exec_list: ExecListRequest, - pub exec_listed: ExecListResponse, - pub write_stdin: WriteStdinRequest, - pub write_stdin_result: WriteStdinResponse, - pub read_output: ReadOutputRequest, - pub output_chunk: ReadOutputResponse, - pub close_stdin: CloseStdinRequest, - pub close_stdin_result: CloseStdinResponse, - pub tty_win_resize: TtyWinResizeRequest, - pub exec_signal: ExecSignalRequest, - pub exec_cancel: ExecCancelRequest, - pub control_ack: ControlAck, - pub shell_attach: ShellAttachRequest, - pub shell_attached: ShellAttachResponse, - pub shell_list: ShellListRequest, - pub shell_listed: ShellListResponse, - pub shell_detach: ShellDetachRequest, - pub shell_detached: ShellDetachResponse, - pub shell_kill: ShellKillRequest, - pub shell_killed: ShellKillResponse, - pub shell_close_attach: ShellCloseAttachRequest, - pub terminal_write_stdin: TerminalWriteStdinRequest, - pub terminal_read_output: TerminalReadOutputRequest, - pub terminal_close_stdin: TerminalCloseStdinRequest, - pub terminal_tty_win_resize: TerminalTtyWinResizeRequest, - pub read_guest_file: ReadGuestFileRequest, - pub read_guest_file_result: ReadGuestFileResponse, - pub usbip_import: UsbipImportRequest, - pub usbip_import_result: UsbipImportResponse, - pub usbip_status: UsbipStatusRequest, - pub usbip_status_result: UsbipStatusResponse, - pub activate_system_start: GuestActivationStartRequest, - pub activate_system_started: GuestActivationStartResponse, - pub activate_system_status: GuestActivationStatusRequest, - pub activate_system_status_result: GuestActivationStatusResponse, - pub audio_status: AudioStatusRequest, - pub audio_status_result: AudioStatusResponse, - pub audio_set: AudioSetRequest, - pub audio_set_result: AudioSetResponse, - pub error: GuestControlError, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestTransportSchema { - pub transport: GuestTransportKind, - pub direction: GuestVsockDirection, - pub guest_control_vsock_port: u32, - pub guest_to_host_observability_port: u32, - pub reserved_side_channel_port: u32, - pub identity_binding: GuestIdentityBinding, - pub ttrpc_frame_cap_bytes: u64, - pub host_connect: GuestHostConnectShape, - pub readiness: GuestReadinessContract, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestTransportKind { - VirtioVsockTtrpc, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestHostConnectShape { - pub request_line: GuestConnectRequestLine, - pub ok_ack: GuestConnectAckLine, - pub ack_value: GuestConnectAckValue, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestConnectAckValue { - OpaqueLocalPort, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestReadinessContract { - pub socket_existence_is_readiness: bool, - pub requires_connect_hello_auth_and_health: bool, - pub pre_ttrpc_failures_are_host_synthesized: bool, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestRequestMetadata { - pub vm_id: GuestVmId, - pub request_id: RequestId, - pub protocol_version: u32, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestExecRequestMetadata { - pub common: GuestRequestMetadata, - pub exec_id: ExecId, - pub guest_boot_id: GuestBootId, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TerminalRequestMetadata { - pub common: GuestRequestMetadata, - pub session_id: TerminalSessionId, - pub guest_boot_id: GuestBootId, - pub kind: TerminalKind, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HelloRequest { - pub metadata: GuestRequestMetadata, - pub host_nonce: GuestNonce, - pub transcript_version: u32, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HelloResponse { - pub guest_nonce: GuestNonce, - pub guest_boot_id: GuestBootId, - pub protocol_version: u32, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct AuthenticateRequest { - pub metadata: GuestRequestMetadata, - pub host_nonce: GuestNonce, - pub guest_nonce: GuestNonce, - pub guest_boot_id: GuestBootId, - pub transcript_version: u32, - pub host_auth_tag: GuestAuthTag, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct AuthenticateResponse { - pub guest_auth_tag: Option, - pub capabilities_hash: Option, - pub health: Option, - pub capabilities: Option, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CapabilitiesRequest { - pub metadata: GuestRequestMetadata, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CapabilitiesResponse { - pub protocol_version: u32, - #[schemars(length(max = 32))] - pub capabilities: Vec, - pub limits: GuestEffectiveLimits, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HealthRequest { - pub metadata: GuestRequestMetadata, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HealthResponse { - pub origin: HealthOrigin, - pub state: HealthState, - pub reason: HealthReason, - pub remediation: HealthRemediation, - pub protocol_version: u32, - #[schemars(length(max = 32))] - pub capabilities: Vec, - #[schemars(length(max = 16))] - pub degraded_subsystems: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum HealthState { - Healthy, - Degraded, - UnavailableOldGeneration, - ListenerAbsent, - TransportUnreachable, - AuthFailed, - ProtocolMismatch, - StaleSession, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum HealthReason { - None, - OldGeneration, - ListenerAbsent, - ConnectRefused, - ConnectTimeout, - EofBeforeAck, - MalformedAck, - AckTooLong, - TransportIo, - AuthTokenRejected, - ProtocolVersionUnsupported, - SessionGenerationMismatch, - ExecSubsystemUnavailable, - LogStorageUnavailable, - QuotaExceeded, - RateLimited, - InternalHealthCheckFailed, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum HealthRemediation { - None, - Retry, - RestartVm, - UpgradeGuest, - CheckAuthToken, - CheckGuestdService, - ReduceLoad, - InspectGuestLogs, -} - -impl HealthResponse { - pub fn is_valid_mapping(&self) -> bool { - use HealthOrigin as Origin; - use HealthReason as Reason; - use HealthRemediation as Remediation; - use HealthState as State; - - matches!( - (self.origin, self.state, self.reason, self.remediation), - ( - Origin::GuestReported, - State::Healthy, - Reason::None, - Remediation::None - ) | ( - Origin::GuestReported, - State::Degraded, - Reason::ExecSubsystemUnavailable - | Reason::LogStorageUnavailable - | Reason::QuotaExceeded - | Reason::RateLimited - | Reason::InternalHealthCheckFailed, - Remediation::Retry - | Remediation::ReduceLoad - | Remediation::InspectGuestLogs - | Remediation::RestartVm, - ) | ( - Origin::HostSynthesized, - State::UnavailableOldGeneration, - Reason::OldGeneration, - Remediation::UpgradeGuest | Remediation::RestartVm, - ) | ( - Origin::HostSynthesized, - State::ListenerAbsent, - Reason::ListenerAbsent, - Remediation::CheckGuestdService | Remediation::RestartVm, - ) | ( - Origin::HostSynthesized, - State::TransportUnreachable, - Reason::ConnectRefused - | Reason::ConnectTimeout - | Reason::EofBeforeAck - | Reason::MalformedAck - | Reason::AckTooLong - | Reason::TransportIo, - Remediation::Retry | Remediation::RestartVm | Remediation::CheckGuestdService, - ) | ( - Origin::HostSynthesized, - State::AuthFailed, - Reason::AuthTokenRejected, - Remediation::CheckAuthToken - ) | ( - Origin::HostSynthesized, - State::ProtocolMismatch, - Reason::ProtocolVersionUnsupported, - Remediation::UpgradeGuest, - ) | ( - Origin::HostSynthesized, - State::StaleSession, - Reason::SessionGenerationMismatch, - Remediation::Retry | Remediation::RestartVm, - ) - ) - } -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestEffectiveLimits { - #[schemars(range(min = 1, max = 1048576))] - pub max_chunk_bytes: u64, - #[schemars(range(min = 1, max = 4194304))] - pub max_recv_message_bytes: u64, - #[schemars(range(max = 16777216))] - pub decoded_write_stdin_bytes_per_connection: u64, - #[schemars(range(max = 4))] - pub write_stdin_handlers_per_connection: u32, - #[schemars(range(max = 1))] - pub stdin_queue_chunks_per_exec: u32, - #[schemars(range(max = 8388608))] - pub stdout_live_buffer_bytes: u64, - #[schemars(range(max = 8388608))] - pub stderr_live_buffer_bytes: u64, - #[schemars(range(max = 4194304))] - pub detached_stdout_log_bytes: u64, - #[schemars(range(max = 4194304))] - pub detached_stderr_log_bytes: u64, - #[schemars(range(max = 1000))] - pub long_poll_timeout_ms: u64, - #[schemars(range(max = 300000))] - pub slow_consumer_grace_ms: u64, - #[schemars(range(max = 256))] - pub exec_sessions_per_vm: u32, - #[schemars(range(max = 64))] - pub attached_sessions_per_vm: u32, - #[schemars(range(max = 512))] - pub pending_read_output_waits_per_stream: u32, - #[schemars(range(max = 512))] - pub pending_exec_waits_per_vm: u32, - #[schemars(range(max = 200))] - pub rpc_rate_per_connection_per_second: u32, - #[schemars(range(max = 1000))] - pub rpc_rate_per_vm_burst: u32, - #[schemars(range(max = 256))] - pub shell_sessions_per_vm: u32, - #[schemars(range(max = 64))] - pub shell_attached_sessions_per_vm: u32, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecCreateRequest { - pub metadata: GuestRequestMetadata, - #[schemars(length(min = 1, max = 128))] - pub argv: Vec, - pub user: Option, - pub cwd: Option, - #[schemars(length(max = 256))] - pub env: Vec, - pub tty: bool, - pub stdin_open: bool, - pub detached: bool, - pub initial_terminal_size: Option, - pub output_policy: OutputPolicy, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct EnvVar { - pub key: EnvKey, - pub value: EnvValue, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TerminalSize { - #[schemars(range(min = 1, max = 65535))] - pub rows: u32, - #[schemars(range(min = 1, max = 65535))] - pub cols: u32, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct OutputPolicy { - pub max_chunk_bytes: u64, - pub max_stdout_log_bytes: u64, - pub max_stderr_log_bytes: u64, - pub slow_consumer_timeout_ms: u64, - pub wait_timeout_ms: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecCreateResponse { - pub exec_id: Option, - pub created_at_monotonic_ns: u64, - pub control_seq: u64, - pub stdout_cursor: u64, - pub stderr_cursor: u64, - pub effective_limits: GuestEffectiveLimits, - pub state: ExecState, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecInspectRequest { - pub metadata: GuestExecRequestMetadata, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecInspectResponse { - pub state: ExecState, - pub visible_terminal_status: Option, - pub stdin_state: StdinState, - pub stdout_start_offset: u64, - pub stdout_end_offset: u64, - pub stderr_start_offset: u64, - pub stderr_end_offset: u64, - pub stdout_dropped_bytes: u64, - pub stderr_dropped_bytes: u64, - pub stdout_truncated_for_retention: bool, - pub stderr_truncated_for_retention: bool, - pub last_control_seq: u64, - pub state_generation: u64, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecWaitRequest { - pub metadata: GuestExecRequestMetadata, - pub timeout_ms: u64, - pub known_state_generation: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecWaitResponse { - pub state: ExecState, - pub visible_terminal_status: Option, - pub state_generation: u64, - pub stdout_start_offset: u64, - pub stdout_end_offset: u64, - pub stderr_start_offset: u64, - pub stderr_end_offset: u64, - pub timed_out: bool, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecLogsRequest { - pub metadata: GuestExecRequestMetadata, - pub stream: OutputStream, - pub offset: u64, - #[schemars(range(min = 1, max = 1048576))] - pub max_len: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecLogsResponse { - pub stream: OutputStream, - pub offset: u64, - pub end_offset: u64, - pub data: GuestOutputBytes, - pub next_offset: u64, - pub eof: bool, - pub start_offset: u64, - pub dropped_bytes: u64, - pub truncated: bool, - pub error: Option, -} - -/// Read-only listing of detached execs visible to the same VM token on the -/// same guest boot. Carries no exec id, argv, or output; the boot id binds -/// the request to the current guest generation. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecListRequest { - pub metadata: GuestRequestMetadata, - pub guest_boot_id: GuestBootId, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecListResponse { - #[schemars(length(max = 32))] - pub entries: Vec, - pub error: Option, -} - -/// One detached exec record summary. Discloses only bounded, non-sensitive -/// metadata: the opaque id, slot, lifecycle state, create time, an argv -/// hash (never the raw argv), and retained-log accounting. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecListEntry { - pub exec_id: ExecId, - #[schemars(range(max = 31))] - pub slot: u32, - pub state: ExecState, - pub create_time_unix: u64, - pub argv_sha256: ArgvSha256, - pub stdout_truncated: bool, - pub stderr_truncated: bool, - pub dropped_bytes: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct WriteStdinRequest { - pub metadata: GuestExecRequestMetadata, - pub offset: u64, - pub data: GuestStdinBytes, - pub close_after: bool, - pub client_deadline_ms: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct WriteStdinResponse { - pub accepted_offset: u64, - pub accepted_len: u64, - pub next_offset: u64, - pub stdin_state: StdinState, - pub blocked_ms: u64, - pub disposition: WriteDisposition, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadOutputRequest { - pub metadata: GuestExecRequestMetadata, - pub stream: OutputStream, - pub offset: u64, - #[schemars(range(min = 1, max = 1048576))] - pub max_len: u64, - pub wait: bool, - pub timeout_ms: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadOutputResponse { - pub stream: OutputStream, - pub offset: u64, - pub end_offset: u64, - pub data: GuestOutputBytes, - pub next_offset: u64, - pub eof: bool, - pub start_offset: u64, - pub dropped_bytes: u64, - pub truncated: bool, - pub timed_out: bool, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CloseStdinRequest { - pub metadata: GuestExecRequestMetadata, - pub offset: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CloseStdinResponse { - pub stdin_state: StdinState, - pub final_offset: u64, - pub disposition: WriteDisposition, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TtyWinResizeRequest { - pub metadata: GuestExecRequestMetadata, - pub control_seq: u64, - #[schemars(range(min = 1, max = 65535))] - pub rows: u32, - #[schemars(range(min = 1, max = 65535))] - pub cols: u32, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecSignalRequest { - pub metadata: GuestExecRequestMetadata, - pub control_seq: u64, - pub signal: u32, - pub target: SignalTarget, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecCancelRequest { - pub metadata: GuestExecRequestMetadata, - pub control_seq: u64, - pub reason: ExecCancelReason, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ControlAck { - pub control_seq: u64, - pub duplicate: bool, - pub error: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestActivationMode { - Switch, - Boot, - Test, - DryActivate, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestActivationState { - Running, - Succeeded, - Failed, - TimedOut, - Lost, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestActivationStartRequest { - pub metadata: GuestRequestMetadata, - pub activation_id: GuestActivationId, - pub switch_script_path: GuestSwitchScriptPath, - pub mode: GuestActivationMode, - #[schemars(range(min = 1, max = 3600000))] - pub timeout_ms: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestActivationStartResponse { - pub activation_id: GuestActivationId, - pub state: GuestActivationState, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestActivationStatusRequest { - pub metadata: GuestRequestMetadata, - pub activation_id: GuestActivationId, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestActivationStatusResponse { - pub activation_id: GuestActivationId, - pub state: GuestActivationState, - pub exit_code: Option, - pub signal: Option, - pub status_code: Option, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellAttachRequest { - pub metadata: GuestRequestMetadata, - pub name: Option, - pub force: bool, - pub initial_terminal_size: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellAttachResponse { - pub session_id: Option, - pub resolved_name: ShellName, - pub state: ShellState, - pub force_evicted: bool, - pub control_seq: u64, - pub output_cursor: u64, - pub effective_limits: GuestEffectiveLimits, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellListRequest { - pub metadata: GuestRequestMetadata, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellListResponse { - pub default_name: ShellName, - #[schemars(length(max = 256))] - pub sessions: Vec, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellListEntry { - pub name: ShellName, - pub state: ShellState, - pub attached: bool, - pub is_default: bool, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellDetachRequest { - pub metadata: GuestRequestMetadata, - pub name: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellKillRequest { - pub metadata: GuestRequestMetadata, - pub name: ShellName, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellCloseAttachRequest { - pub metadata: TerminalRequestMetadata, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellDetachResponse { - pub resolved_name: ShellName, - pub detached: bool, - pub cause: ShellCloseCause, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellKillResponse { - pub name: ShellName, - pub killed: bool, - pub state: ShellState, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TerminalWriteStdinRequest { - pub metadata: TerminalRequestMetadata, - pub offset: u64, - pub data: GuestStdinBytes, - pub close_after: bool, - pub client_deadline_ms: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TerminalReadOutputRequest { - pub metadata: TerminalRequestMetadata, - pub stream: OutputStream, - pub offset: u64, - #[schemars(range(min = 1, max = 1048576))] - pub max_len: u64, - pub wait: bool, - pub timeout_ms: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TerminalCloseStdinRequest { - pub metadata: TerminalRequestMetadata, - pub offset: u64, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TerminalTtyWinResizeRequest { - pub metadata: TerminalRequestMetadata, - pub control_seq: u64, - #[schemars(range(min = 1, max = 65535))] - pub rows: u32, - #[schemars(range(min = 1, max = 65535))] - pub cols: u32, -} - -/// Framework read of a host-declared guest file, keyed by a closed enum -/// (NOT a free-form path). guestd maps the key to the host-declared target. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadGuestFileRequest { - pub metadata: GuestRequestMetadata, - pub file_id: GuestFileId, -} - -/// Single-shot bounded response. `content` is capped at -/// `READ_GUEST_FILE_MAX_BYTES`; oversize files fail with `FileTooLarge` BEFORE -/// any read/allocation. `size_bytes`/`sha256` are guest-reported diagnostics -/// only - the host recomputes both from the received bytes. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadGuestFileResponse { - pub file_id: GuestFileId, - pub size_bytes: u64, - pub content: GuestFileBytes, - pub sha256: ArgvSha256, - pub error: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum UsbipImportAction { - Attach, - Detach, -} - -/// Guest-side USBIP lifecycle operation. `host` and `bus_id` are host-declared -/// values resolved by d2bd from the trusted manifest; guestd validates both -/// before invoking `usbip`. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct UsbipImportRequest { - pub metadata: GuestRequestMetadata, - pub action: UsbipImportAction, - pub host: GuestUsbipHost, - pub bus_id: GuestUsbipBusId, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct UsbipImportResponse { - pub action: UsbipImportAction, - pub bus_id: GuestUsbipBusId, - #[schemars(range(max = 32))] - pub detached_ports: u32, - pub error: Option, -} - -/// Side-effect-free guest USBIP import observation. Optional filters are -/// validated with the same closed host/bus-id rules as `UsbipImport`. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct UsbipStatusRequest { - pub metadata: GuestRequestMetadata, - pub host: Option, - pub bus_id: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct UsbipStatusResponse { - #[schemars(length(max = 64))] - pub imports: Vec, - pub error: Option, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct UsbipStatusEntry { - #[schemars(range(max = 65535))] - pub port: u32, - pub host: GuestUsbipHost, - #[schemars(range(min = 1, max = 65535))] - pub tcp_port: u32, - pub bus_id: GuestUsbipBusId, -} - -// ── Audio RPC types (ADR 0041) ─────────────────────────────────────────────── - -/// Current state of one audio channel as seen by the guest PipeWire session. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestAudioChannelState { - /// True when the channel is muted (grant = off). - pub muted: bool, - /// Current level 0-100; 0 when not configured or unavailable. - pub level: u32, - /// True when the level value is meaningful. - pub level_known: bool, -} - -/// Request: query current guest audio policy state. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct AudioStatusRequest { - pub metadata: GuestRequestMetadata, -} - -/// Response: per-channel guest audio state. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct AudioStatusResponse { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub microphone: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub speaker: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -/// Which audio mutation to perform. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum AudioSetKind { - /// Turn a channel on (unmute) or off (mute). - Grant, - /// Adjust the channel level (volume/gain), `0..=100`. - Level, -} - -/// The audio channel to target. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum AudioChannel { - Microphone, - Speaker, -} - -/// Request: mutate one audio channel's grant or level in the guest session. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct AudioSetRequest { - pub metadata: GuestRequestMetadata, - pub channel: AudioChannel, - pub kind: AudioSetKind, - /// For `Grant`: `true` = on, `false` = off. - pub grant_on: bool, - /// For `Level`: `0..=100`; ignored for `Grant`. - pub level: u32, -} - -/// Response: updated channel state after the mutation. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct AudioSetResponse { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub state: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum OutputStream { - Stdout, - Stderr, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum WriteDisposition { - Accepted, - Duplicate, - Rejected, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum ExecState { - Created, - Running, - Exited, - Signaled, - Cancelled, - SlowConsumerCancelled, - ProtocolError, - LostGuestd, - Reaped, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum StdinState { - Open, - Closing, - Closed, - ClosedByProcess, - RejectedNotInteractive, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde( - rename_all = "kebab-case", - rename_all_fields = "camelCase", - tag = "outcome", - deny_unknown_fields -)] -pub enum TerminalStatus { - ExitCode { - #[serde(rename = "exitCode")] - exit_code: i32, - }, - Signal { - signal: u32, - }, - StatusCode { - #[serde(rename = "statusCode")] - status_code: i32, - }, - Error { - error: GuestControlErrorKind, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum SignalTarget { - ForegroundProcessGroup, - ProcessTree, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum ExecCancelReason { - ClientDisconnect, - UserRequested, - SlowConsumer, - ProtocolError, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GuestControlError { - pub kind: GuestControlErrorKind, - pub remediation: HealthRemediation, - pub retry_after_ms: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum GuestControlErrorKind { - ProtocolError, - MaxChunkExceeded, - StdinBackpressure, - StdinClosed, - StdinNotOpen, - StdinClosedByProcess, - StdinOffsetMismatch, - StdinByteBudgetExhausted, - OffsetExpired, - OffsetInFuture, - OffsetExhausted, - OutputLost, - TtyStderrUnavailable, - TtyRequired, - ExecCapacityExceeded, - ExecAttachCapacityExceeded, - ExecNotFound, - ExecAlreadyExited, - GuestExecDisabled, - GuestExecRootDenied, - GuestExecUserDenied, - CwdInvalid, - CwdDenied, - RetainedLogPathUnsafe, - RetainedLogQuotaExceeded, - ReadWaitCapacityExceeded, - WaitCapacityExceeded, - SupersededReadWait, - RateLimited, - RequestIdConflict, - ControlSeqMismatch, - SlowConsumerCancelled, - StaleSession, - GuestControlUnavailableOldGeneration, - AuthFailed, - TransportUnreachable, - ExecExpired, - FileNotFound, - FileTooLarge, - PathUnsafe, - ReadDenied, - InvalidProgram, - UsbipUnavailable, - UsbipCommandFailed, - UsbipInvalidBusId, - UsbipInvalidHost, - GuestShellDisabled, - ShellInvalidName, - ShellCapacityExceeded, - ShellAttachCapacityExceeded, - ShellNotFound, - ShellAlreadyAttached, - ShellPoolUnavailable, - ShellDaemonEpochMismatch, - ShellOutputGap, - UsbipCommandTimeout, - UsbipInvalidOutput, - ActivationInvalidId, - ActivationInvalidPath, - ActivationInvalidMode, - ActivationNotFound, - ActivationStatusUnavailable, - ActivationTimedOut, - ActivationSpawnFailed, - /// Guest PipeWire session is unavailable or not configured. - #[serde(rename = "audio-pipewire-unavailable")] - AudioPipeWireUnavailable, - /// Requested audio channel is not recognised. - AudioChannelUnknown, - /// Audio level value is out of the 0-100 range. - AudioLevelOutOfRange, - /// PipeWire enforcement call succeeded but the policy could not be applied. - AudioEnforcementFailed, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::guest_proto as pb; - use protobuf::{Message, MessageField}; - - fn proto_round_trip(message: M) - where - M: Message + Default + PartialEq + std::fmt::Debug, - { - let encoded = message.write_to_bytes().expect("message encodes"); - let decoded = M::parse_from_bytes(&encoded).expect("message decodes"); - assert_eq!(decoded, message); - } - - #[test] - fn base64_encoded_len_matches_standard_padded_expansion() { - assert_eq!(base64_encoded_len(0), 0); - assert_eq!(base64_encoded_len(1), 4); - assert_eq!(base64_encoded_len(2), 4); - assert_eq!(base64_encoded_len(3), 4); - assert_eq!(base64_encoded_len(4), 8); - // 512 KiB raw → 699 KiB encoded, comfortably under the 1 MiB frame. - assert_eq!(base64_encoded_len(512 * 1024), 699_052); - } - - #[test] - fn guest_config_encoded_cap_boundaries_for_both_framings() { - let cap = READ_GUEST_CONFIG_ENCODED_MAX_BYTES; - // cap-1 and cap fit; cap+1 fails closed. - assert!(guest_config_encoded_within_frame_caps(cap - 1)); - assert!(guest_config_encoded_within_frame_caps(cap)); - assert!(!guest_config_encoded_within_frame_caps(cap + 1)); - } - - #[test] - #[allow(clippy::assertions_on_constants)] - fn guest_config_encoded_cap_fits_public_sock_and_ttrpc_frames() { - // The derived encoded cap (plus envelope margin) must sit under the - // public.sock frame and the ttRPC frame - the binding cap is the - // smaller public.sock frame. - assert!( - READ_GUEST_CONFIG_ENCODED_MAX_BYTES + READ_GUEST_CONFIG_ENVELOPE_MARGIN - < d2b_contracts::MAX_FRAME_SIZE - ); - assert!((READ_GUEST_CONFIG_ENCODED_MAX_BYTES as u64) < TTRPC_FRAME_CAP_BYTES); - // A payload at the public.sock frame size must be rejected by the cap. - assert!(!guest_config_encoded_within_frame_caps( - d2b_contracts::MAX_FRAME_SIZE - )); - } - - #[test] - fn shell_name_shape_is_validated_on_deserialize() { - for name in ["default", "ops_1", "A.B-c_9", "_scratch"] { - let decoded: ShellName = - serde_json::from_value(serde_json::json!(name)).expect("valid shell name"); - assert_eq!(decoded.0, name); - } - - let too_long = "a".repeat(65); - for name in [ - "", - ".", - "..", - "-default", - "bad/name", - "bad name", - "bad{name}", - too_long.as_str(), - ] { - serde_json::from_value::(serde_json::json!(name)) - .expect_err("invalid shell name rejects"); - } - } - - #[test] - fn health_mappings_are_closed() { - let valid = HealthResponse { - origin: HealthOrigin::HostSynthesized, - state: HealthState::TransportUnreachable, - reason: HealthReason::MalformedAck, - remediation: HealthRemediation::Retry, - protocol_version: GUEST_CONTROL_PROTOCOL_VERSION, - capabilities: Vec::new(), - degraded_subsystems: Vec::new(), - }; - assert!(valid.is_valid_mapping()); - - let invalid = HealthResponse { - origin: HealthOrigin::GuestReported, - state: HealthState::Healthy, - reason: HealthReason::AuthTokenRejected, - remediation: HealthRemediation::CheckAuthToken, - protocol_version: GUEST_CONTROL_PROTOCOL_VERSION, - capabilities: Vec::new(), - degraded_subsystems: Vec::new(), - }; - assert!(!invalid.is_valid_mapping()); - } - - #[test] - fn enums_serialize_kebab_case() { - assert_eq!( - serde_json::to_string(&HealthState::UnavailableOldGeneration).unwrap(), - "\"unavailable-old-generation\"" - ); - assert_eq!( - serde_json::to_string(&GuestControlErrorKind::TtyStderrUnavailable).unwrap(), - "\"tty-stderr-unavailable\"" - ); - assert_eq!( - serde_json::to_string(&GuestControlErrorKind::ExecExpired).unwrap(), - "\"exec-expired\"" - ); - assert_eq!( - serde_json::to_string(&GuestControlErrorKind::UsbipCommandFailed).unwrap(), - "\"usbip-command-failed\"" - ); - assert_eq!( - serde_json::to_string(&GuestControlErrorKind::UsbipInvalidHost).unwrap(), - "\"usbip-invalid-host\"" - ); - assert_eq!( - serde_json::to_string(&UsbipImportAction::Attach).unwrap(), - "\"attach\"" - ); - assert_eq!( - serde_json::to_string(&GuestActivationMode::DryActivate).unwrap(), - "\"dry-activate\"" - ); - assert_eq!( - serde_json::to_string(&GuestControlErrorKind::ActivationTimedOut).unwrap(), - "\"activation-timed-out\"" - ); - assert_eq!( - serde_json::to_string(&TerminalStatus::Signal { signal: 2 }).unwrap(), - "{\"outcome\":\"signal\",\"signal\":2}" - ); - } - - #[test] - fn generated_usbip_status_shapes_round_trip() { - let mut common = pb::RequestMetadata::new(); - common.vm_id = "corp-vm".to_owned(); - common.request_id = "req-usbip-status".to_owned(); - common.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - - let mut request = pb::UsbipStatusRequest::new(); - request.metadata = MessageField::some(common); - request.host = Some("192.0.2.1".to_owned()); - request.bus_id = Some("1-2.1".to_owned()); - proto_round_trip(request); - - let mut entry = pb::UsbipStatusEntry::new(); - entry.port = 1; - entry.host = "192.0.2.1".to_owned(); - entry.tcp_port = 3240; - entry.bus_id = "1-2.1".to_owned(); - let mut response = pb::UsbipStatusResponse::new(); - response.imports.push(entry); - proto_round_trip(response); - - assert_eq!( - pb::GuestCapability::GUEST_CAPABILITY_USBIP_STATUS as i32, - 14, - "generated USBIP status capability discriminant changed" - ); - assert_eq!( - pb::GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_TIMEOUT as i32, - 56, - "generated USBIP timeout discriminant changed" - ); - assert_eq!( - pb::GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_OUTPUT as i32, - 57, - "generated USBIP invalid-output discriminant changed" - ); - } -} diff --git a/packages/d2b-contracts-control/src/lib.rs b/packages/d2b-contracts-control/src/lib.rs index 9ca55bc31f..f581fe0c8e 100644 --- a/packages/d2b-contracts-control/src/lib.rs +++ b/packages/d2b-contracts-control/src/lib.rs @@ -1,14 +1,7 @@ #![doc = "Canonical guest and public control-plane wire contracts for d2b."] pub mod cli_output; -pub mod generated; -pub mod guest_auth; -pub mod guest_wire; pub mod public_wire; pub mod proxy_readiness; pub mod terminal_wire; pub mod unsafe_local_wire; - -pub mod guest_proto { - pub use crate::generated::guest_control::*; -} diff --git a/packages/d2b-contracts-control/src/public_wire.rs b/packages/d2b-contracts-control/src/public_wire.rs index 802f49a21c..3eb7783aa5 100644 --- a/packages/d2b-contracts-control/src/public_wire.rs +++ b/packages/d2b-contracts-control/src/public_wire.rs @@ -1,4 +1,3 @@ -use crate::guest_wire::ExecState; pub use d2b_contracts::audit_wire::{AuditExportCursor, AuditExportEntry}; use d2b_contracts::types::MediaRef; use d2b_contracts::{ @@ -23,6 +22,22 @@ use schemars::{ use serde::{Deserialize, Serialize}; use std::fmt; +/// Lifecycle state projected for a target-local Process or +/// EphemeralProcess resource. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum ExecState { + Created, + Running, + Exited, + Signaled, + Cancelled, + SlowConsumerCancelled, + ProtocolError, + LostTarget, + Reaped, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind", content = "payload")] #[allow(clippy::large_enum_variant)] @@ -94,24 +109,9 @@ pub enum PublicRequest { /// The CLI exposes this as `d2b host reconcile --network --apply`. #[serde(rename = "host reconcile")] HostReconcile(HostReconcileRequest), - /// Host-wide cutover and scoped-reset operation family. - #[serde(rename = "host cutover")] - HostCutover(HostCutoverRequest), - /// Read the editable guest config working copy of `vm` over the - /// authenticated guest-control bridge and return it as a base64 string. - /// ADMIN-ONLY (it crosses into the guest over the authenticated - /// transport): the daemon enforces `PeerRole::Admin` BEFORE any probe / - /// sign / read. The CLI's `config sync` uses this on guest-control VMs - /// instead of an SSH transfer. - #[serde(rename = "read guest config")] - ReadGuestConfig(ReadGuestConfigRequest), /// Multiplexed, ADMIN-ONLY operation on a daemon-held authenticated - /// guest-control exec session. A single owner connection issues a - /// `Start` op then drives the session with the remaining ops - /// (`WriteStdin`/`ReadOutput`/`Signal`/`Resize`/`Wait`/`Close`). The - /// daemon enforces `PeerRole::Admin` BEFORE any session lookup, vsock - /// connect, auth, or `ExecCreate`. `d2b vm exec` drives this verb; - /// it never crosses SSH. + /// Process resource session. A single owner connection issues a `Start` + /// op then drives the session with named-stream operations. #[serde(rename = "exec")] Exec(ExecOp), /// Console streaming operation (ADR 0041). @@ -159,9 +159,6 @@ pub enum PublicResponse { Audit(AuditResponse), #[serde(rename = "host check")] HostCheck(HostCheckResponse), - /// Host-wide cutover or scoped-reset response. - #[serde(rename = "host cutover")] - HostCutover(HostCutoverResponse), #[serde(rename = "keys list")] KeysList(KeysListResponse), #[serde(rename = "keys show")] @@ -172,8 +169,6 @@ pub enum PublicResponse { StoreVerify(StoreVerifyResponse), #[serde(rename = "mutating verb")] MutatingVerb(MutatingVerbResponse), - #[serde(rename = "read guest config")] - ReadGuestConfig(ReadGuestConfigResponse), #[serde(rename = "exec")] Exec(ExecOpResponse), #[serde(rename = "console")] @@ -515,6 +510,9 @@ fn is_false(value: &bool) -> bool { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ActivationRequest { pub vm: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 1))] + pub to_generation: Option, #[serde(default, flatten)] pub flags: MutationFlags, } @@ -586,32 +584,11 @@ pub struct StoreVerifyRequest { pub type StoreVerifyResponse = d2b_contracts::store_verify_wire::StoreVerifyResponse; -/// `read guest config` request payload. The daemon resolves the per-VM vsock -/// socket + peer credentials from the trusted bundle; the client supplies only -/// the VM name. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadGuestConfigRequest { - pub vm: String, -} - -/// `read guest config` response payload. `contentBase64` is the standard -/// padded base64 of the RAW guest config bytes. The encoded payload is bounded -/// by `READ_GUEST_CONFIG_ENCODED_MAX_BYTES` (derived from -/// `READ_GUEST_FILE_MAX_BYTES`) so it always fits within the public.sock and -/// ttRPC frames. The CLI decodes it and computes size + sha256 from the -/// DECODED bytes - never from any guest-reported value. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadGuestConfigResponse { - pub content_base64: String, -} - /// Maximum decoded stdin chunk per `WriteStdin` op and decoded output chunk -/// per `ReadOutput` op (`DEFAULT_MAX_CHUNK_BYTES`). The base64 envelope of a +/// per `ReadOutput` op. The base64 envelope of a /// 64 KiB chunk (~87 KiB) stays well under the 1 MiB public.sock frame, so a /// single exec op never approaches the frame cap. -pub const EXEC_MAX_CHUNK_BYTES: u64 = crate::guest_wire::DEFAULT_MAX_CHUNK_BYTES; +pub const EXEC_MAX_CHUNK_BYTES: u64 = 64 * 1024; /// Output stream selector for `ReadOutput`. A closed enum - the daemon never /// forwards an unspecified stream to the guest. @@ -1258,6 +1235,258 @@ pub enum ExecOpResponse { Kill(ExecDetachedKillResult), } +/// Request carried by an authenticated Process or ShellSession named stream. +/// +/// The stream itself binds the resource and caller, so no session handle, +/// execution target, user, argv, environment, cwd, or transport locator is +/// repeated in these messages. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde( + tag = "kind", + content = "payload", + rename_all = "camelCase", + deny_unknown_fields +)] +pub enum NamedProcessStreamRequest { + /// Send one bounded stdin chunk. + Stdin { + offset: u64, + chunk_base64: String, + #[serde(default)] + eof: bool, + }, + /// Request one bounded output chunk. + Read { + stream: ExecStream, + offset: u64, + max_len: u64, + #[serde(default)] + wait: bool, + #[serde(default)] + timeout_ms: u64, + }, + /// Deliver one allowlisted signal to the process. + Signal { control_seq: u64, signo: u32 }, + /// Resize the attached terminal. + Resize { + control_seq: u64, + rows: u32, + cols: u32, + }, + /// Half-close stdin without tearing down the process. + CloseStdin { offset: u64 }, + /// Cancel the attached process and named stream. + Cancel, + /// Cleanly close the attached named stream without cancelling its process. + Close, + /// Poll the process terminal state. + Wait { timeout_ms: u64 }, +} + +impl fmt::Debug for NamedProcessStreamRequest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stdin { + offset, + chunk_base64, + eof, + } => formatter + .debug_struct("NamedProcessStreamRequest::Stdin") + .field("offset", offset) + .field("chunk_base64_len", &chunk_base64.len()) + .field("eof", eof) + .finish(), + Self::Read { + stream, + offset, + max_len, + wait, + timeout_ms, + } => formatter + .debug_struct("NamedProcessStreamRequest::Read") + .field("stream", stream) + .field("offset", offset) + .field("max_len", max_len) + .field("wait", wait) + .field("timeout_ms", timeout_ms) + .finish(), + Self::Signal { control_seq, signo } => formatter + .debug_struct("NamedProcessStreamRequest::Signal") + .field("control_seq", control_seq) + .field("signo", signo) + .finish(), + Self::Resize { + control_seq, + rows, + cols, + } => formatter + .debug_struct("NamedProcessStreamRequest::Resize") + .field("control_seq", control_seq) + .field("rows", rows) + .field("cols", cols) + .finish(), + Self::CloseStdin { offset } => formatter + .debug_struct("NamedProcessStreamRequest::CloseStdin") + .field("offset", offset) + .finish(), + Self::Cancel => formatter.write_str("NamedProcessStreamRequest::Cancel"), + Self::Close => formatter.write_str("NamedProcessStreamRequest::Close"), + Self::Wait { timeout_ms } => formatter + .debug_struct("NamedProcessStreamRequest::Wait") + .field("timeout_ms", timeout_ms) + .finish(), + } + } +} + +/// Response carried by an authenticated Process or ShellSession named stream. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde( + tag = "kind", + content = "payload", + rename_all = "camelCase", + deny_unknown_fields +)] +pub enum NamedProcessStreamResponse { + /// Acknowledge a stdin write. + Stdin(ExecWriteStdinResult), + /// Return one output chunk. + Output(ExecReadOutputResult), + /// Acknowledge a control operation. + Delivered(ExecControlResult), + /// Return the current terminal state. + Wait(ExecWaitResult), + /// Confirm stdin half-close or clean named-stream close. + Closed(ExecCloseResult), + /// Return a terminal disposition. + Terminal(ExecTerminalStatus), + /// Refuse the stream operation with a closed redacted error class. + Error(NamedProcessStreamError), +} + +impl fmt::Debug for NamedProcessStreamResponse { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stdin(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Stdin") + .field(result) + .finish(), + Self::Output(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Output") + .field(result) + .finish(), + Self::Delivered(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Delivered") + .field(result) + .finish(), + Self::Wait(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Wait") + .field(result) + .finish(), + Self::Closed(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Closed") + .field(result) + .finish(), + Self::Terminal(result) => match result { + ExecTerminalStatus::Exited { code } => formatter + .debug_struct("NamedProcessStreamResponse::Terminal") + .field("kind", &"exited") + .field("code", code) + .finish(), + ExecTerminalStatus::Signaled { signal } => formatter + .debug_struct("NamedProcessStreamResponse::Terminal") + .field("kind", &"signaled") + .field("signal", signal) + .finish(), + ExecTerminalStatus::Error { .. } => formatter + .debug_struct("NamedProcessStreamResponse::Terminal") + .field("kind", &"error") + .finish(), + }, + Self::Error(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Error") + .field(result) + .finish(), + } + } +} + +/// Closed failure vocabulary for named Process/ShellSession streams. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum NamedProcessStreamErrorKind { + /// The authenticated session cannot perform this operation. + Authorization, + /// The resource or controller generation is stale. + StaleSession, + /// The named stream or process is no longer available. + NotFound, + /// The stream credit or process quota is exhausted. + Backpressure, + /// The request was malformed or violated the resource contract. + Protocol, + /// The bounded operation deadline elapsed. + Timeout, + /// The authenticated ComponentSession disconnected. + Disconnected, +} + +/// Redacted named-stream failure. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NamedProcessStreamError { + pub kind: NamedProcessStreamErrorKind, +} + +impl fmt::Debug for NamedProcessStreamError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("NamedProcessStreamError") + .field("kind", &self.kind) + .finish() + } +} + +/// Correlated request frame carried by a Process/ShellSession stream. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NamedProcessStreamRequestFrame { + /// Bounded in-stream correlation id. + pub request_id: u64, + /// The typed operation. + pub request: NamedProcessStreamRequest, +} + +impl NamedProcessStreamRequestFrame { + /// Construct one correlated request frame. + pub const fn new(request_id: u64, request: NamedProcessStreamRequest) -> Self { + Self { + request_id, + request, + } + } +} + +/// Correlated response frame carried by a Process/ShellSession stream. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NamedProcessStreamResponseFrame { + /// Request id being completed. + pub request_id: u64, + /// The typed operation result. + pub response: NamedProcessStreamResponse, +} + +impl NamedProcessStreamResponseFrame { + /// Construct one correlated response frame. + pub const fn new(request_id: u64, response: NamedProcessStreamResponse) -> Self { + Self { + request_id, + response, + } + } +} + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] #[serde(transparent)] pub struct ShellName(String); @@ -1536,8 +1765,8 @@ pub enum ConsoleProviderKind { LocalHypervisor, /// qemu-media VM with a broker-owned fd-backed console backend. QemuMedia, - /// ACA sandbox with a guestd-compatible agent over the provider peer - /// transport (no local socket or broker fd involved). + /// ACA sandbox with a provider agent over the provider peer transport + /// (no local socket or broker fd involved). AcaSandbox, } @@ -1834,13 +2063,13 @@ pub enum AudioChannel { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum AudioEnforcementPosture { - /// Host-side PipeWire policy and guest-side guestd policy both applied. + /// Host-side PipeWire policy and target-local Process policy both applied. HostAndGuest, - /// Host-side enforcement only; guestd is absent or the provider does not - /// support guest enforcement. + /// Host-side enforcement only; the provider does not support target-local + /// enforcement. HostOnly, - /// Guest-side guestd enforcement only; no local PipeWire node (e.g. - /// ACA sandbox with a guestd-compatible agent). + /// Target-local Process enforcement only; no local PipeWire node (for + /// example, an ACA sandbox with a provider agent). GuestOnly, /// Neither host nor guest enforcement is supported for this target. Unsupported, @@ -1857,8 +2086,8 @@ pub enum AudioEnforcementPosture { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum AudioErrorKind { - /// The provider is expected to expose a guestd-compatible agent but none - /// was found; operator remediation is required. + /// The provider is expected to expose a target-local audio Process but + /// none was found; operator remediation is required. ProviderMisconfigured, /// The requested VM was not found in the bundle. VmNotFound, @@ -1880,7 +2109,7 @@ pub enum AudioProviderKind { LocalHypervisor, /// qemu-media VM with a declared qemu audio backend. QemuMedia, - /// ACA sandbox with a guestd-compatible agent for audio policy. + /// ACA sandbox with a provider agent for audio policy. AcaSandbox, } @@ -1984,9 +2213,10 @@ pub struct AudioStatusResult { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum AudioSetApplied { - /// Applied to both host (PipeWire/qemu) and guest (guestd). + /// Applied to both host (PipeWire/qemu) and the target-local Process. HostAndGuest, - /// Applied to host only; guestd enforcement was unavailable or degraded. + /// Applied to host only; target-local enforcement was unavailable or + /// degraded. HostOnly, /// Applied to guest only (ACA sandbox; no local host audio state written). GuestOnly, @@ -2068,151 +2298,6 @@ pub struct HostReconcileRequest { pub network: bool, } -/// One host cutover or scoped-reset command. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HostCutoverRequest { - /// Requested operation. - pub operation: HostCutoverOperation, - /// Existing operation identity for status or continuation. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub operation_id: Option, - /// Candidate identity bound to the request. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub candidate_id: Option, - /// Revision-plan identity bound to the request. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub revision_plan_id: Option, - /// System artifact identity bound to the frozen cutover preview and handoff. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub system_artifact_id: Option, - /// Preserved source artifact identity bound to native rollback. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_system_artifact_id: Option, - /// Exact preview digest. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub preview_digest: Option, - /// Qualified recovery digest for cutover apply. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub recovery_digest: Option, - /// Bound operator identity. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub operator_id: Option, - /// Exact apply or finalization consent digest. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub consent_digest: Option, - /// Canonical serialized U3 apply consent. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub consent_json: Option, - /// Separate destructive reset consent digest. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub destructive_consent_digest: Option, - /// Canonical destructive reset consent artifact. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub destructive_consent_json: Option, - /// Whether this reset explicitly authorizes durable-Volume destruction. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub destroy_durable_volumes: Option, - /// Canonical serialized qualified recovery attestation. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub recovery_attestation_json: Option, - /// Host identity digest bound by the recovery attestation. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_digest: Option, - /// Fresh consent used by a non-owner Admin to resume. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fresh_consent_digest: Option, - /// Bounded hold reason. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, - /// Scoped reset authority, required only for `reset`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reset_scope: Option, - /// Opaque target identity for a scoped reset. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub target: Option, - /// Zone selection is forbidden for one-time cutover operations. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub zone: Option, -} - -/// Closed host cutover command vocabulary. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum HostCutoverOperation { - /// Build a mutation-free preview. - Preview, - /// Read the current redacted status. - Status, - /// Set an operator hold. - Hold, - /// Clear a hold and resume. - Resume, - /// Consume apply consent and launch the runner. - Apply, - /// Roll back before the native boundary. - Rollback, - /// Verify every configured Zone. - Verify, - /// Read-only cutover health diagnostics. - Doctor, - /// Consume phase-10 consent. - Finalize, - /// Run a distinct scoped reset. - Reset, -} - -/// Closed scoped-reset authority vocabulary. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "kebab-case")] -pub enum HostCutoverResetScope { - /// Reset one Zone. - Zone, - /// Reset one Provider. - Provider, - /// Reset one Guest. - Guest, -} - -/// Redaction-safe host cutover response. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HostCutoverResponse { - /// Command that produced this response. - pub operation: HostCutoverOperation, - /// Opaque operation identity, when one exists. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub operation_id: Option, - /// Redacted U3 operation state. - pub state: String, - /// Current protocol phase number. - pub phase: u8, - /// Preview digest, when the operation has a preview. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub preview_digest: Option, - /// Redaction-safe summary. - pub summary: String, - /// Whether this response represents an accepted mutation. - pub mutation_accepted: bool, - /// Inventory summary without paths, secrets, or host identifiers. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub inventory: Option, -} - -/// Redaction-safe inventory summary for a host-wide preview. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HostCutoverInventorySummary { - /// Number of configured Zones included. - pub zone_count: u32, - /// Number of classified inventory items. - pub item_count: u32, - /// Canonical inventory digest. - pub inventory_digest: String, - /// Whether every configured Zone was observed. - pub complete: bool, -} - /// Mutating-verb daemon response shape. /// /// `outcome = "dry-run-planned"` returns a human-readable plan @@ -2888,7 +2973,10 @@ fn default_true() -> bool { #[cfg(test)] mod tests { use super::{ - AuditResponse, LevelPercent, MutationFlags, PublicRequest, PublicResponse, RuntimeSummary, + AuditResponse, ExecReadOutputResult, ExecStream, ExecTerminalStatus, LevelPercent, + MutationFlags, NamedProcessStreamError, NamedProcessStreamErrorKind, + NamedProcessStreamRequest, NamedProcessStreamRequestFrame, NamedProcessStreamResponse, + NamedProcessStreamResponseFrame, PublicRequest, PublicResponse, RuntimeSummary, VmLifecycleRequest, VmLifecycleState, }; use d2b_contracts::{ @@ -2904,6 +2992,82 @@ mod tests { assert_eq!(encoded, "\"Booted\""); } + #[test] + fn named_process_stream_frames_round_trip_without_identity_fields() { + let request = NamedProcessStreamRequest::Stdin { + offset: 7, + chunk_base64: "c2VjcmV0".to_owned(), + eof: false, + }; + let value = serde_json::to_value(&request).expect("request serializes"); + assert_eq!(value["kind"], "stdin"); + assert!(value.get("session").is_none()); + let decoded: NamedProcessStreamRequest = + serde_json::from_value(value).expect("request decodes"); + assert_eq!(decoded, request); + let close = NamedProcessStreamRequest::Close; + assert_eq!( + serde_json::from_value::( + serde_json::to_value(&close).expect("close serializes") + ) + .expect("close decodes"), + close + ); + let frame = NamedProcessStreamRequestFrame::new(11, request); + let decoded: NamedProcessStreamRequestFrame = + serde_json::from_value(serde_json::to_value(&frame).expect("request frame serializes")) + .expect("request frame decodes"); + assert_eq!(decoded.request_id, 11); + + let response = NamedProcessStreamResponse::Output(ExecReadOutputResult { + data_base64: "c2VjcmV0".to_owned(), + next_offset: 13, + eof: false, + dropped_bytes: 0, + truncated: false, + timed_out: false, + }); + let rendered = format!("{response:?}"); + assert!(!rendered.contains("c2VjcmV0")); + let terminal = NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Exited { code: 0 }); + assert_eq!( + serde_json::from_value::( + serde_json::to_value(terminal).expect("terminal serializes") + ) + .expect("terminal decodes"), + NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Exited { code: 0 }) + ); + let terminal_error = NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Error { + slug: "D2B_TERMINAL_LEAK_CANARY".to_owned(), + }); + assert!(!format!("{terminal_error:?}").contains("D2B_TERMINAL_LEAK_CANARY")); + let error = NamedProcessStreamResponse::Error(NamedProcessStreamError { + kind: NamedProcessStreamErrorKind::Disconnected, + }); + assert_eq!( + serde_json::from_value::( + serde_json::to_value(error).expect("error serializes") + ) + .expect("error decodes"), + NamedProcessStreamResponse::Error(NamedProcessStreamError { + kind: NamedProcessStreamErrorKind::Disconnected, + }) + ); + let response_frame = NamedProcessStreamResponseFrame::new( + 11, + NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Exited { code: 0 }), + ); + assert_eq!( + serde_json::from_value::( + serde_json::to_value(response_frame).expect("response frame serializes") + ) + .expect("response frame decodes") + .request_id, + 11 + ); + let _ = ExecStream::Stdout; + } + #[test] fn public_response_deserializes_success_envelopes() { let capability_value = serde_json::json!({ diff --git a/packages/d2b-contracts-control/src/terminal_wire.rs b/packages/d2b-contracts-control/src/terminal_wire.rs index e5d502773d..6278d3ebe6 100644 --- a/packages/d2b-contracts-control/src/terminal_wire.rs +++ b/packages/d2b-contracts-control/src/terminal_wire.rs @@ -557,14 +557,14 @@ mod tests { let wait = ExecWaitResult { running: false, terminal_status: Some(crate::public_wire::ExecTerminalStatus::Error { - slug: "lost-guestd".to_owned(), + slug: "component-session-target-lost".to_owned(), }), }; let terminal: crate::terminal_wire::TerminalWaitResult = wait.clone().into(); assert_eq!( terminal.terminal_status, Some(TerminalStatus::Error { - slug: "lost-guestd".to_owned() + slug: "component-session-target-lost".to_owned() }) ); let round_trip: ExecWaitResult = terminal.into(); diff --git a/packages/d2b-contracts-control/tests/guest_control.rs b/packages/d2b-contracts-control/tests/guest_control.rs deleted file mode 100644 index 30ca40ae60..0000000000 --- a/packages/d2b-contracts-control/tests/guest_control.rs +++ /dev/null @@ -1,264 +0,0 @@ -use d2b_contracts_control::guest_proto as pb; -use d2b_contracts_control::guest_wire::{ - GUEST_CONTROL_PROTOCOL_VERSION, GUEST_CONTROL_VSOCK_PORT, HARD_MAX_CHUNK_BYTES, -}; -use protobuf::{EnumOrUnknown, Message, MessageField}; - -fn round_trip(message: M) -where - M: Message + Default + PartialEq + std::fmt::Debug, -{ - let encoded = message.write_to_bytes().expect("message encodes"); - let decoded = M::parse_from_bytes(&encoded).expect("message decodes"); - assert_eq!(decoded, message); -} - -#[test] -fn generated_health_round_trips_with_guest_wire_constants() { - let mut health = pb::HealthResponse::new(); - health.origin = EnumOrUnknown::new(pb::HealthOrigin::HEALTH_ORIGIN_GUEST_REPORTED); - health.state = EnumOrUnknown::new(pb::HealthState::HEALTH_STATE_HEALTHY); - health.reason = EnumOrUnknown::new(pb::HealthReason::HEALTH_REASON_NONE); - health.remediation = EnumOrUnknown::new(pb::HealthRemediation::HEALTH_REMEDIATION_NONE); - health.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - health.capabilities.push(EnumOrUnknown::new( - pb::GuestCapability::GUEST_CAPABILITY_HEALTH, - )); - - round_trip(health); -} - -#[test] -fn generated_hello_and_capabilities_shapes_compile() { - let mut metadata = pb::RequestMetadata::new(); - metadata.vm_id = "corp-vm".to_owned(); - metadata.request_id = "req-1".to_owned(); - metadata.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - - let mut hello = pb::HelloRequest::new(); - hello.metadata = MessageField::some(metadata.clone()); - hello.host_nonce = vec![1; 32]; - hello.transcript_version = 1; - round_trip(hello); - - let mut hello_response = pb::HelloResponse::new(); - hello_response.guest_nonce = vec![2; 32]; - hello_response.guest_boot_id = "boot-id".to_owned(); - hello_response.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - round_trip(hello_response); - - let mut auth = pb::AuthenticateRequest::new(); - auth.metadata = MessageField::some(metadata.clone()); - auth.host_nonce = vec![1; 32]; - auth.guest_nonce = vec![2; 32]; - auth.guest_boot_id = "boot-id".to_owned(); - auth.transcript_version = 1; - auth.host_auth_tag = vec![3; 32]; - round_trip(auth); - - let mut health = pb::HealthResponse::new(); - health.origin = EnumOrUnknown::new(pb::HealthOrigin::HEALTH_ORIGIN_GUEST_REPORTED); - health.state = EnumOrUnknown::new(pb::HealthState::HEALTH_STATE_HEALTHY); - health.reason = EnumOrUnknown::new(pb::HealthReason::HEALTH_REASON_NONE); - health.remediation = EnumOrUnknown::new(pb::HealthRemediation::HEALTH_REMEDIATION_NONE); - health.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - - let mut limits = pb::GuestEffectiveLimits::new(); - limits.max_chunk_bytes = HARD_MAX_CHUNK_BYTES; - limits.max_recv_message_bytes = 4 * 1024 * 1024; - - let mut caps = pb::CapabilitiesResponse::new(); - caps.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - caps.capabilities.push(EnumOrUnknown::new( - pb::GuestCapability::GUEST_CAPABILITY_HEALTH, - )); - caps.limits = MessageField::some(limits); - round_trip(caps.clone()); - - let mut authenticated = pb::AuthenticateResponse::new(); - authenticated.guest_auth_tag = Some(vec![4; 32]); - authenticated.capabilities_hash = Some("capabilities-sha256".to_owned()); - authenticated.health = MessageField::some(health); - authenticated.capabilities = MessageField::some(caps); - round_trip(authenticated); -} - -#[test] -fn generated_exec_message_shapes_compile_without_service_stubs() { - let mut common = pb::RequestMetadata::new(); - common.vm_id = "corp-vm".to_owned(); - common.request_id = "req-2".to_owned(); - common.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - - let mut exec_metadata = pb::ExecRequestMetadata::new(); - exec_metadata.common = MessageField::some(common.clone()); - exec_metadata.exec_id = "exec-1".to_owned(); - exec_metadata.guest_boot_id = "boot-1".to_owned(); - - let mut create = pb::ExecCreateRequest::new(); - create.metadata = MessageField::some(common); - create.argv = vec!["/bin/true".to_owned()]; - create.user = Some("alice".to_owned()); - create.tty = false; - round_trip(create); - - let mut logs = pb::ReadOutputRequest::new(); - logs.metadata = MessageField::some(exec_metadata.clone()); - logs.stream = EnumOrUnknown::new(pb::OutputStream::OUTPUT_STREAM_STDOUT); - logs.max_len = HARD_MAX_CHUNK_BYTES; - round_trip(logs); - - let mut status = pb::TerminalStatus::new(); - status.outcome = Some(pb::terminal_status::Outcome::ExitCode(0)); - round_trip(status); - - let mut signal = pb::ExecSignalRequest::new(); - signal.metadata = MessageField::some(exec_metadata); - signal.signal = 15; - signal.target = EnumOrUnknown::new(pb::SignalTarget::SIGNAL_TARGET_PROCESS_TREE); - round_trip(signal); -} - -#[test] -fn generated_transport_schema_constants_match_guest_wire() { - assert_eq!(GUEST_CONTROL_VSOCK_PORT, 14_318); - assert_eq!( - pb::GuestCapability::GUEST_CAPABILITY_HEALTH as i32, - 1, - "generated enum discriminant changed" - ); - assert_eq!( - pb::HealthState::HEALTH_STATE_HEALTHY as i32, - 1, - "generated health enum discriminant changed" - ); -} - -#[test] -fn generated_exec_expired_error_kind_matches_guest_wire() { - // The additive `ExecExpired` wire kind is value 37 and must stay in - // lockstep with the `guest_wire::GuestControlErrorKind` enum. - assert_eq!( - pb::GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_EXEC_EXPIRED as i32, - 37, - "generated ExecExpired discriminant changed" - ); - assert_eq!( - serde_json::to_string( - &d2b_contracts_control::guest_wire::GuestControlErrorKind::ExecExpired - ) - .unwrap(), - "\"exec-expired\"", - ); -} - -#[test] -fn generated_exec_list_shapes_round_trip() { - let mut common = pb::RequestMetadata::new(); - common.vm_id = "corp-vm".to_owned(); - common.request_id = "req-list".to_owned(); - common.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - - let mut request = pb::ExecListRequest::new(); - request.metadata = MessageField::some(common); - request.guest_boot_id = "boot-1".to_owned(); - round_trip(request); - - let mut entry = pb::ExecListEntry::new(); - entry.exec_id = "00112233".to_owned(); - entry.slot = 7; - entry.state = EnumOrUnknown::new(pb::ExecState::EXEC_STATE_RUNNING); - entry.create_time_unix = 1_700_000_000; - entry.argv_sha256 = "a".repeat(64); - entry.stdout_truncated = true; - entry.stderr_truncated = false; - entry.dropped_bytes = 4096; - - let mut response = pb::ExecListResponse::new(); - response.entries.push(entry); - round_trip(response); -} - -#[test] -fn generated_usbip_import_shapes_round_trip() { - let mut common = pb::RequestMetadata::new(); - common.vm_id = "corp-vm".to_owned(); - common.request_id = "req-usbip".to_owned(); - common.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - - let mut request = pb::UsbipImportRequest::new(); - request.metadata = MessageField::some(common); - request.action = EnumOrUnknown::new(pb::UsbipImportAction::USBIP_IMPORT_ACTION_ATTACH); - request.host = "192.0.2.1".to_owned(); - request.bus_id = "1-2.1".to_owned(); - round_trip(request); - - let mut response = pb::UsbipImportResponse::new(); - response.action = EnumOrUnknown::new(pb::UsbipImportAction::USBIP_IMPORT_ACTION_ATTACH); - response.bus_id = "1-2.1".to_owned(); - response.detached_ports = 1; - round_trip(response); - - assert_eq!( - pb::GuestCapability::GUEST_CAPABILITY_USBIP_IMPORT as i32, - 10, - "generated USBIP capability discriminant changed" - ); - assert_eq!( - pb::GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_COMMAND_FAILED as i32, - 44, - "generated USBIP command-failed discriminant changed" - ); - assert_eq!( - pb::GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_USBIP_INVALID_HOST as i32, - 46, - "generated USBIP invalid-host discriminant changed" - ); -} - -#[test] -fn generated_activation_shapes_round_trip() { - let mut common = pb::RequestMetadata::new(); - common.vm_id = "corp-vm".to_owned(); - common.request_id = "req-activation".to_owned(); - common.protocol_version = GUEST_CONTROL_PROTOCOL_VERSION; - - let mut start = pb::GuestActivationStartRequest::new(); - start.metadata = MessageField::some(common.clone()); - start.activation_id = "01234567-89ab-cdef-0123-456789abcdef".to_owned(); - start.switch_script_path = - "/nix/store/0123456789abcdfghijklmnpqrsvwxyz-nixos-system/bin/switch-to-configuration" - .to_owned(); - start.mode = EnumOrUnknown::new(pb::GuestActivationMode::GUEST_ACTIVATION_MODE_SWITCH); - start.timeout_ms = 600_000; - round_trip(start); - - let mut start_response = pb::GuestActivationStartResponse::new(); - start_response.activation_id = "01234567-89ab-cdef-0123-456789abcdef".to_owned(); - start_response.state = - EnumOrUnknown::new(pb::GuestActivationState::GUEST_ACTIVATION_STATE_RUNNING); - round_trip(start_response); - - let mut status = pb::GuestActivationStatusRequest::new(); - status.metadata = MessageField::some(common); - status.activation_id = "01234567-89ab-cdef-0123-456789abcdef".to_owned(); - round_trip(status); - - let mut status_response = pb::GuestActivationStatusResponse::new(); - status_response.activation_id = "01234567-89ab-cdef-0123-456789abcdef".to_owned(); - status_response.state = - EnumOrUnknown::new(pb::GuestActivationState::GUEST_ACTIVATION_STATE_SUCCEEDED); - status_response.exit_code = Some(0); - round_trip(status_response); - - assert_eq!( - pb::GuestCapability::GUEST_CAPABILITY_SYSTEM_ACTIVATION as i32, - 15, - "generated activation capability discriminant changed" - ); - assert_eq!( - pb::GuestControlErrorKind::GUEST_CONTROL_ERROR_KIND_ACTIVATION_TIMED_OUT as i32, - 63, - "generated activation timeout discriminant changed" - ); -} diff --git a/packages/d2b-contracts-provider/src/v3/provider.rs b/packages/d2b-contracts-provider/src/v3/provider.rs index 0cd37504db..81d29ee875 100644 --- a/packages/d2b-contracts-provider/src/v3/provider.rs +++ b/packages/d2b-contracts-provider/src/v3/provider.rs @@ -40,7 +40,9 @@ use d2b_contracts_resource::v3::{ ResourceTypeName, SchemaFingerprint, resource::ResourceEnvelope, - resource_schema::{CanonicalJsonObject, ExtensionSchemaId, SchemaVersion}, + resource_schema::{ + CanonicalJsonObject, ExtensionSchemaId, PlacementAnchor, SchemaVersion, + }, volume::{MAX_VIEWS, ViewRight, ViewSpec}, volume_state::{MigrationPolicy, PersistenceClass, SensitivityClass, VolumeStateSchema, VolumeStateSchemaId}, }; @@ -65,7 +67,12 @@ pub const MAX_COMPONENT_RESOURCE_TYPES: usize = 8; /// Maximum methods one component exports. pub const MAX_COMPONENT_METHODS: usize = 32; - +/// Maximum required EffectPort classes on one target capability. +pub const MAX_COMPONENT_EFFECT_CLASSES: usize = 16; +/// Maximum target capability declarations on one component. +pub const MAX_COMPONENT_TARGET_CAPABILITIES: usize = 3; +/// Maximum signed component cardinality. +pub const MAX_COMPONENT_CARDINALITY: u32 = 256; /// Maximum entries in one signed standard capability matrix. pub const MAX_CAPABILITY_MATRIX_ENTRIES: usize = 32; @@ -140,6 +147,30 @@ pub enum ProviderContractError { PlacementHostCustodyViolation, /// The schema category requires Guest-local custody. GuestLocalRequired, + /// A controller role omitted its closed instance scope. + ControllerScopeMissing, + /// A controller role declared a target set incompatible with its scope. + ControllerScopeInvalid, + /// A target capability was declared more than once. + DuplicateTargetCapability, + /// A target capability does not carry a concrete signed artifact. + ComponentArtifactMissing, + /// A controller's Host and Guest target artifacts differ. + ComponentArtifactMismatch, + /// The component execution mode is not valid for its signed role. + ComponentExecutionInvalid, + /// A placement anchor does not match the registered ResourceType schema. + PlacementAnchorMismatch, + /// A ResourceApiBinding omitted its placement anchor. + PlacementAnchorMissing, + /// The shared daemon or broker artifact differs between Host and Guest. + SharedRuntimeArtifactMismatch, + /// A target-local component has no matching shared runtime artifacts. + RuntimeArtifactMissing, + /// The manifest advertises a target without a component capability. + TargetCapabilityMissing, + /// A target profile does not support a signed required EffectPort class. + EffectClassUnsupported, } impl core::fmt::Display for ProviderContractError { @@ -189,6 +220,18 @@ impl ProviderContractError { Self::ComponentQuotaTooSmall => "component-quota-too-small", Self::PlacementHostCustodyViolation => "placement-host-custody-violation", Self::GuestLocalRequired => "guest-local-required", + Self::ControllerScopeMissing => "controller-scope-missing", + Self::ControllerScopeInvalid => "controller-scope-invalid", + Self::DuplicateTargetCapability => "duplicate-target-capability", + Self::ComponentArtifactMissing => "component-artifact-missing", + Self::ComponentArtifactMismatch => "component-artifact-mismatch", + Self::ComponentExecutionInvalid => "component-execution-invalid", + Self::PlacementAnchorMismatch => "placement-anchor-mismatch", + Self::PlacementAnchorMissing => "placement-anchor-missing", + Self::SharedRuntimeArtifactMismatch => "shared-runtime-artifact-mismatch", + Self::RuntimeArtifactMissing => "runtime-artifact-missing", + Self::TargetCapabilityMissing => "target-capability-missing", + Self::EffectClassUnsupported => "effect-class-unsupported", } } } @@ -557,6 +600,196 @@ pub enum ComponentType { Worker, } +/// The closed instance cardinality model for a signed controller role. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "kebab-case")] +pub enum ControllerInstanceScope { + /// One controller for the containing Zone. + ZoneSingleton, + /// One controller at one fixed Host or Guest target. + FixedExecutionTarget, + /// One controller instance per resolved resource target. + PerResourceTarget, +} + +/// The target kinds a signed component may advertise. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "lowercase")] +pub enum ControllerTargetKind { + /// The containing Zone, used only by Zone-singleton roles. + Zone, + /// The physical Host execution target. + Host, + /// A workload or nested Guest execution target. + Guest, +} + +/// A closed Provider-neutral EffectPort class. +/// +/// These are capability classes, not broker operation names. A target profile +/// intersects the signed set before a component receives any effect port. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "kebab-case")] +pub enum EffectPortClass { + Runtime, + Transport, + Substrate, + Process, + Volume, + Storage, + Network, + Device, + Display, + Audio, + Credential, + Observability, +} + +/// A signed component artifact and its required effect classes for one target. +#[derive(Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ComponentTargetCapability { + target_kind: ControllerTargetKind, + artifact_digest: ArtifactDigest, + required_effect_classes: BTreeSet, +} + +impl ComponentTargetCapability { + /// Construct one target capability with a concrete signed artifact. + pub fn new( + target_kind: ControllerTargetKind, + artifact_digest: ArtifactDigest, + required_effect_classes: impl IntoIterator, + ) -> Result { + let required_effect_classes: BTreeSet<_> = required_effect_classes.into_iter().collect(); + if required_effect_classes.len() > MAX_COMPONENT_EFFECT_CLASSES { + return Err(ProviderContractError::BoundExceeded); + } + Ok(Self { + target_kind, + artifact_digest, + required_effect_classes, + }) + } + + /// Return the target kind this capability serves. + pub const fn target_kind(&self) -> ControllerTargetKind { + self.target_kind + } + + /// Borrow the concrete signed component artifact digest. + pub const fn artifact_digest(&self) -> &ArtifactDigest { + &self.artifact_digest + } + + /// Borrow the required EffectPort classes. + pub const fn required_effect_classes(&self) -> &BTreeSet { + &self.required_effect_classes + } +} + +impl core::fmt::Debug for ComponentTargetCapability { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("ComponentTargetCapability") + .field("target_kind", &self.target_kind) + .field("effect_class_count", &self.required_effect_classes.len()) + .finish_non_exhaustive() + } +} + +impl<'de> Deserialize<'de> for ComponentTargetCapability { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + struct Wire { + target_kind: ControllerTargetKind, + artifact_digest: ArtifactDigest, + #[serde(default)] + required_effect_classes: BTreeSet, + } + let wire = Wire::deserialize(deserializer)?; + Self::new( + wire.target_kind, + wire.artifact_digest, + wire.required_effect_classes, + ) + .map_err(serde::de::Error::custom) + } +} + +/// Shared daemon and broker artifacts selected for one target kind. +#[derive(Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct TargetRuntimeArtifacts { + target_kind: ControllerTargetKind, + d2bd_digest: ArtifactDigest, + broker_digest: ArtifactDigest, +} + +impl TargetRuntimeArtifacts { + /// Construct one target runtime artifact declaration. + pub fn new( + target_kind: ControllerTargetKind, + d2bd_digest: ArtifactDigest, + broker_digest: ArtifactDigest, + ) -> Result { + if matches!(target_kind, ControllerTargetKind::Zone) { + return Err(ProviderContractError::ControllerScopeInvalid); + } + Ok(Self { + target_kind, + d2bd_digest, + broker_digest, + }) + } + + /// Return the target kind. + pub const fn target_kind(&self) -> ControllerTargetKind { + self.target_kind + } + + /// Borrow the shared `d2bd` artifact digest. + pub const fn d2bd_digest(&self) -> &ArtifactDigest { + &self.d2bd_digest + } + + /// Borrow the shared broker artifact digest. + pub const fn broker_digest(&self) -> &ArtifactDigest { + &self.broker_digest + } +} + +impl core::fmt::Debug for TargetRuntimeArtifacts { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("TargetRuntimeArtifacts") + .field("target_kind", &self.target_kind) + .finish_non_exhaustive() + } +} + +impl<'de> Deserialize<'de> for TargetRuntimeArtifacts { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + struct Wire { + target_kind: ControllerTargetKind, + d2bd_digest: ArtifactDigest, + broker_digest: ArtifactDigest, + } + let wire = Wire::deserialize(deserializer)?; + Self::new(wire.target_kind, wire.d2bd_digest, wire.broker_digest) + .map_err(serde::de::Error::custom) + } +} + /// The closed dependency alias set a manifest may declare. /// /// Zone config binds each alias to an exact Provider ResourceRef and service @@ -1111,6 +1344,12 @@ pub struct ComponentDescriptor { exported_methods: BTreeSet, allowed_domains: BTreeSet, cardinality: u32, + #[serde(skip_serializing_if = "Option::is_none")] + instance_scope: Option, + #[serde(skip_serializing_if = "BTreeSet::is_empty")] + supported_target_kinds: BTreeSet, + #[serde(skip_serializing_if = "Vec::is_empty")] + target_capabilities: Vec, config_digest: ArtifactDigest, dependencies: BTreeSet, declares_state_volume: bool, @@ -1149,6 +1388,7 @@ impl ComponentDescriptor { || exported_methods.len() > MAX_COMPONENT_METHODS || allowed_domains.is_empty() || cardinality == 0 + || cardinality > MAX_COMPONENT_CARDINALITY { return Err(ProviderContractError::BoundExceeded); } @@ -1188,6 +1428,9 @@ impl ComponentDescriptor { exported_methods, allowed_domains, cardinality, + instance_scope: None, + supported_target_kinds: BTreeSet::new(), + target_capabilities: Vec::new(), config_digest, dependencies: dependency_set, declares_state_volume, @@ -1259,6 +1502,142 @@ impl ComponentDescriptor { self.cardinality } + /// Set the closed controller instance scope and supported target kinds. + pub fn with_controller_placement( + mut self, + instance_scope: ControllerInstanceScope, + supported_target_kinds: impl IntoIterator, + ) -> Result { + let supported_target_kinds: BTreeSet<_> = supported_target_kinds.into_iter().collect(); + if supported_target_kinds.is_empty() { + return Err(ProviderContractError::ControllerScopeMissing); + } + self.instance_scope = Some(instance_scope); + self.supported_target_kinds = supported_target_kinds; + Ok(self) + } + + /// Set the signed target-specific component artifacts and effect classes. + pub fn with_target_capabilities( + mut self, + target_capabilities: impl IntoIterator, + ) -> Result { + let target_capabilities: Vec<_> = target_capabilities.into_iter().collect(); + if target_capabilities.len() > MAX_COMPONENT_TARGET_CAPABILITIES { + return Err(ProviderContractError::BoundExceeded); + } + let mut targets = BTreeSet::new(); + for capability in &target_capabilities { + if !targets.insert(capability.target_kind()) { + return Err(ProviderContractError::DuplicateTargetCapability); + } + } + self.target_capabilities = target_capabilities; + Ok(self) + } + + /// Return the controller instance scope, if this is a controller role. + pub const fn instance_scope(&self) -> Option { + self.instance_scope + } + + /// Borrow the controller target kinds this descriptor advertises. + pub const fn supported_target_kinds(&self) -> &BTreeSet { + &self.supported_target_kinds + } + + /// Borrow target-specific signed component capabilities. + pub fn target_capabilities(&self) -> &[ComponentTargetCapability] { + &self.target_capabilities + } + + /// Find the target capability for one target kind. + pub fn target_capability( + &self, + target_kind: ControllerTargetKind, + ) -> Option<&ComponentTargetCapability> { + self.target_capabilities + .iter() + .find(|capability| capability.target_kind() == target_kind) + } + + fn validate_role_contract( + &self, + artifact_id: &ArtifactId, + ) -> Result<(), ProviderContractError> { + let capability_targets: BTreeSet<_> = self + .target_capabilities + .iter() + .map(ComponentTargetCapability::target_kind) + .collect(); + + match self.component_type { + ComponentType::Controller => { + if capability_targets != self.supported_target_kinds { + return Err(ProviderContractError::TargetCapabilityMissing); + } + let scope = self + .instance_scope + .ok_or(ProviderContractError::ControllerScopeMissing)?; + let valid_scope = match scope { + ControllerInstanceScope::ZoneSingleton => { + self.cardinality == 1 + && self.supported_target_kinds + == BTreeSet::from([ControllerTargetKind::Zone]) + } + ControllerInstanceScope::FixedExecutionTarget => { + self.cardinality == 1 + && self.supported_target_kinds.len() == 1 + && !self + .supported_target_kinds + .contains(&ControllerTargetKind::Zone) + } + ControllerInstanceScope::PerResourceTarget => { + self.cardinality > 0 + && !self.supported_target_kinds.is_empty() + && !self + .supported_target_kinds + .contains(&ControllerTargetKind::Zone) + } + }; + if !valid_scope { + return Err(ProviderContractError::ControllerScopeInvalid); + } + let host_artifact = self + .target_capability(ControllerTargetKind::Host) + .map(ComponentTargetCapability::artifact_digest); + let guest_artifact = self + .target_capability(ControllerTargetKind::Guest) + .map(ComponentTargetCapability::artifact_digest); + if let (Some(host), Some(guest)) = (host_artifact, guest_artifact) { + if host != guest { + return Err(ProviderContractError::ComponentArtifactMismatch); + } + } + if matches!(self.execution, ComponentExecution::InProcessBootstrap) + && !matches!( + artifact_id.as_str(), + "system-core" | "system-minijail" + ) + { + return Err(ProviderContractError::ComponentExecutionInvalid); + } + } + ComponentType::Service | ComponentType::Worker => { + if self.instance_scope.is_some() || !self.supported_target_kinds.is_empty() { + return Err(ProviderContractError::ControllerScopeInvalid); + } + if self.target_capabilities.is_empty() { + return Err(ProviderContractError::TargetCapabilityMissing); + } + if matches!(self.execution, ComponentExecution::InProcessBootstrap) { + return Err(ProviderContractError::ComponentExecutionInvalid); + } + } + } + Ok(()) + } + /// The digest of this component's config projection. pub const fn config_digest(&self) -> &ArtifactDigest { &self.config_digest @@ -1310,6 +1689,12 @@ impl<'de> Deserialize<'de> for ComponentDescriptor { exported_methods: BTreeSet, allowed_domains: BTreeSet, cardinality: u32, + #[serde(default)] + instance_scope: Option, + #[serde(default)] + supported_target_kinds: BTreeSet, + #[serde(default)] + target_capabilities: Vec, config_digest: ArtifactDigest, #[serde(default)] dependencies: BTreeSet, @@ -1343,6 +1728,16 @@ impl<'de> Deserialize<'de> for ComponentDescriptor { false, ) .map(|descriptor| descriptor.with_execution(execution)) + .and_then(|descriptor| { + let descriptor = if let Some(scope) = wire.instance_scope { + descriptor.with_controller_placement(scope, wire.supported_target_kinds)? + } else if !wire.supported_target_kinds.is_empty() { + return Err(ProviderContractError::ControllerScopeInvalid); + } else { + descriptor + }; + descriptor.with_target_capabilities(wire.target_capabilities) + }) .and_then(|descriptor| descriptor.with_state_namespaces(wire.state_namespaces)) .map_err(serde::de::Error::custom) } @@ -1466,6 +1861,8 @@ redacted_debug!(ExtensionSchemaRegistration); #[serde(rename_all = "camelCase")] pub struct ResourceApiBinding { resource_type: ResourceTypeName, + #[serde(skip_serializing_if = "Option::is_none")] + placement_anchor: Option, base_spec_version: SchemaVersion, base_spec_fingerprint: SchemaFingerprint, base_status_version: SchemaVersion, @@ -1490,6 +1887,67 @@ impl ResourceApiBinding { spec_extension: Option, status_extension: Option, ) -> Result { + let placement_anchor = PlacementAnchor::canonical_for(&resource_type); + Self::new_inner( + resource_type, + placement_anchor, + base_spec_version, + base_spec_fingerprint, + base_status_version, + base_status_fingerprint, + capability_matrix, + spec_extension, + status_extension, + ) + } + + /// Construct a ResourceApiBinding with an explicit closed placement + /// anchor. + #[allow(clippy::too_many_arguments)] + pub fn new_with_placement( + resource_type: ResourceTypeName, + base_spec_version: SchemaVersion, + base_spec_fingerprint: SchemaFingerprint, + base_status_version: SchemaVersion, + base_status_fingerprint: SchemaFingerprint, + capability_matrix: StandardCapabilityMatrix, + spec_extension: Option, + status_extension: Option, + placement_anchor: PlacementAnchor, + ) -> Result { + placement_anchor + .validate_for(&resource_type) + .map_err(|_| ProviderContractError::PlacementAnchorMismatch)?; + Self::new_inner( + resource_type, + Some(placement_anchor), + base_spec_version, + base_spec_fingerprint, + base_status_version, + base_status_fingerprint, + capability_matrix, + spec_extension, + status_extension, + ) + } + + #[allow(clippy::too_many_arguments)] + fn new_inner( + resource_type: ResourceTypeName, + placement_anchor: Option, + base_spec_version: SchemaVersion, + base_spec_fingerprint: SchemaFingerprint, + base_status_version: SchemaVersion, + base_status_fingerprint: SchemaFingerprint, + capability_matrix: StandardCapabilityMatrix, + spec_extension: Option, + status_extension: Option, + ) -> Result { + if let Some(placement_anchor) = placement_anchor { + placement_anchor + .validate_for(&resource_type) + .map_err(|_| ProviderContractError::PlacementAnchorMismatch)?; + } for registration in [spec_extension.as_ref(), status_extension.as_ref()] .into_iter() .flatten() @@ -1500,6 +1958,7 @@ impl ResourceApiBinding { } Ok(Self { resource_type, + placement_anchor, base_spec_version, base_spec_fingerprint, base_status_version, @@ -1515,6 +1974,12 @@ impl ResourceApiBinding { &self.resource_type } + /// Return the registered placement anchor, if this binding was built + /// through the legacy constructor for an unknown qualified type. + pub const fn placement_anchor(&self) -> Option<&PlacementAnchor> { + self.placement_anchor.as_ref() + } + /// The base spec schema version this binding implements. pub const fn base_spec_version(&self) -> SchemaVersion { self.base_spec_version @@ -1570,6 +2035,8 @@ impl<'de> Deserialize<'de> for ResourceApiBinding { #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Wire { resource_type: ResourceTypeName, + #[serde(default)] + placement_anchor: Option, base_spec_version: SchemaVersion, base_spec_fingerprint: SchemaFingerprint, base_status_version: SchemaVersion, @@ -1582,8 +2049,9 @@ impl<'de> Deserialize<'de> for ResourceApiBinding { status_extension: Option, } let wire = Wire::deserialize(deserializer)?; - Self::new( + Self::new_inner( wire.resource_type, + wire.placement_anchor, wire.base_spec_version, wire.base_spec_fingerprint, wire.base_status_version, @@ -1936,6 +2404,8 @@ pub struct ProviderManifest { components: Vec, api_bindings: Vec, projection_factories: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + runtime_artifacts: Vec, upgrade_policy: UpgradePolicy, } @@ -1987,6 +2457,9 @@ impl ProviderManifest { } let mut bound_types = BTreeSet::new(); for binding in &api_bindings { + if binding.placement_anchor().is_none() { + return Err(ProviderContractError::PlacementAnchorMissing); + } if !bound_types.insert(binding.resource_type().clone()) { return Err(ProviderContractError::DuplicateDeclaration); } @@ -1994,6 +2467,11 @@ impl ProviderManifest { return Err(ProviderContractError::MissingRequiredField); } } + if owned_types.iter().any(|resource_type| { + !bound_types.contains(resource_type) + }) { + return Err(ProviderContractError::MissingRequiredField); + } for factory in &projection_factories { if !bound_types.contains(factory.service_type()) { return Err(ProviderContractError::ProjectionFactoryInvalid); @@ -2017,10 +2495,134 @@ impl ProviderManifest { components, api_bindings, projection_factories, + runtime_artifacts: Vec::new(), upgrade_policy, }) } + /// Validate the shared Host and Guest daemon/broker artifact declarations. + pub fn validate_runtime_artifacts( + entries: impl IntoIterator, + ) -> Result<(), ProviderContractError> { + let entries: Vec<_> = entries.into_iter().collect(); + if entries.len() > 3 { + return Err(ProviderContractError::BoundExceeded); + } + let mut seen = BTreeSet::new(); + for entry in &entries { + if !seen.insert(entry.target_kind()) { + return Err(ProviderContractError::DuplicateDeclaration); + } + } + let host = entries + .iter() + .find(|entry| entry.target_kind() == ControllerTargetKind::Host); + let guest = entries + .iter() + .find(|entry| entry.target_kind() == ControllerTargetKind::Guest); + if let (Some(host), Some(guest)) = (host, guest) + && (host.d2bd_digest() != guest.d2bd_digest() + || host.broker_digest() != guest.broker_digest()) + { + return Err(ProviderContractError::SharedRuntimeArtifactMismatch); + } + Ok(()) + } + + /// Attach the signed shared daemon and broker artifacts to this manifest. + pub fn with_target_runtime_artifacts( + mut self, + entries: impl IntoIterator, + ) -> Result { + let mut entries: Vec<_> = entries.into_iter().collect(); + Self::validate_runtime_artifacts(entries.clone())?; + entries.sort_by_key(TargetRuntimeArtifacts::target_kind); + self.runtime_artifacts = entries; + Ok(self) + } + + /// Validate every placement, component artifact, effect, and shared + /// runtime declaration needed before installation. + pub fn validate_installation_contract(&self) -> Result<(), ProviderContractError> { + for component in &self.components { + component.validate_role_contract(&self.artifact_id)?; + if component.component_type() == ComponentType::Controller { + let scope = component + .instance_scope() + .ok_or(ProviderContractError::ControllerScopeMissing)?; + let expected_anchor = match scope { + ControllerInstanceScope::ZoneSingleton => PlacementAnchor::Zone, + ControllerInstanceScope::FixedExecutionTarget + | ControllerInstanceScope::PerResourceTarget => PlacementAnchor::ExecutionRef, + }; + for resource_type in component.exported_resource_types() { + let binding = self + .binding_for(resource_type) + .ok_or(ProviderContractError::MissingRequiredField)?; + let placement_anchor = binding + .placement_anchor() + .ok_or(ProviderContractError::PlacementAnchorMissing)?; + if PlacementAnchor::canonical_for(resource_type) + .is_some_and(|canonical_anchor| canonical_anchor != *placement_anchor) + { + return Err(ProviderContractError::PlacementAnchorMismatch); + } + if *placement_anchor != expected_anchor { + return Err(ProviderContractError::ControllerScopeInvalid); + } + } + } + } + let mut required_targets = BTreeSet::new(); + for component in &self.components { + for capability in component.target_capabilities() { + if matches!( + capability.target_kind(), + ControllerTargetKind::Host | ControllerTargetKind::Guest + ) { + required_targets.insert(capability.target_kind()); + } + } + } + Self::validate_runtime_artifacts(self.runtime_artifacts.clone())?; + for target in required_targets { + if !self + .runtime_artifacts + .iter() + .any(|entry| entry.target_kind() == target) + { + return Err(ProviderContractError::RuntimeArtifactMissing); + } + } + Ok(()) + } + + /// Validate the signed EffectPort requirements against one target + /// profile's supported classes. + pub fn validate_target_effects( + &self, + target_kind: ControllerTargetKind, + supported_effect_classes: &BTreeSet, + ) -> Result<(), ProviderContractError> { + let mut advertised = false; + for component in &self.components { + let Some(capability) = component.target_capability(target_kind) else { + continue; + }; + advertised = true; + if !capability + .required_effect_classes() + .is_subset(supported_effect_classes) + { + return Err(ProviderContractError::EffectClassUnsupported); + } + } + if !advertised { + return Err(ProviderContractError::TargetCapabilityMissing); + } + Ok(()) + } + /// The artifact identifier a Provider spec selects this manifest with. pub const fn artifact_id(&self) -> &ArtifactId { &self.artifact_id @@ -2056,6 +2658,11 @@ impl ProviderManifest { &self.projection_factories } + /// Borrow the shared daemon and broker artifacts by target kind. + pub fn runtime_artifacts(&self) -> &[TargetRuntimeArtifacts] { + &self.runtime_artifacts + } + /// The upgrade, drain, and restart policy. pub const fn upgrade_policy(&self) -> &UpgradePolicy { &self.upgrade_policy @@ -2091,6 +2698,7 @@ impl ProviderManifest { required_fingerprint: &SchemaFingerprint, ) -> Result<(), ProviderContractError> { self.trust.admit()?; + self.validate_installation_contract()?; self.compatibility .admits(required_major, required_minor, required_fingerprint) } @@ -2120,6 +2728,8 @@ impl<'de> Deserialize<'de> for ProviderManifest { api_bindings: Vec, #[serde(default)] projection_factories: Vec, + #[serde(default)] + runtime_artifacts: Vec, upgrade_policy: UpgradePolicy, } let wire = Wire::deserialize(deserializer)?; @@ -2133,6 +2743,9 @@ impl<'de> Deserialize<'de> for ProviderManifest { wire.projection_factories, wire.upgrade_policy, ) + .and_then(|manifest| { + manifest.with_target_runtime_artifacts(wire.runtime_artifacts) + }) .map_err(serde::de::Error::custom) } } @@ -2219,7 +2832,7 @@ mod tests { "sha256:0000000000000000000000000000000000000000000000000000000000000002"; const MINIMAL_PROVIDER_SPEC: &[u8] = br#"{"artifactId":"provider-wayland","config":{}}"#; - const STATEFUL_COMPONENT: &[u8] = br#"{"allowedDomains":["system"],"cardinality":1,"componentId":"volume-controller","componentType":"controller","configDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","declaresStateVolume":true,"dependencies":[{"alias":"volume","required":true}],"exportedMethods":["assess-update"],"exportedResourceTypes":["Volume"],"stateNamespaces":[{"id":"main-state","kind":"state","migrationPolicy":"pre-launch-required","persistenceClass":"persistent","placementMode":"guest-local","quotaBytes":4096,"schemaDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","schemaId":"example-provider.d2bus.org/controller/main-state","schemaVersion":"1.0","sealingRequired":false,"sensitivityClass":"private","storageNeed":"secret","views":{"main":{"rights":["read","write","create","delete","traverse"]}}}]}"#; + const STATEFUL_COMPONENT: &[u8] = br#"{"allowedDomains":["system"],"binaryRef":"volume-controller","cardinality":1,"componentId":"volume-controller","componentType":"controller","configDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","declaresStateVolume":true,"dependencies":[{"alias":"volume","required":true}],"exportedMethods":["assess-update"],"exportedResourceTypes":["Volume"],"instanceScope":"per-resource-target","stateNamespaces":[{"id":"main-state","kind":"state","migrationPolicy":"pre-launch-required","persistenceClass":"persistent","placementMode":"guest-local","quotaBytes":4096,"schemaDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","schemaId":"example-provider.d2bus.org/controller/main-state","schemaVersion":"1.0","sealingRequired":false,"sensitivityClass":"private","storageNeed":"secret","views":{"main":{"rights":["read","write","create","delete","traverse"]}}}],"supportedTargetKinds":["host","guest"],"targetCapabilities":[{"artifactDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","requiredEffectClasses":["storage"],"targetKind":"host"},{"artifactDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","requiredEffectClasses":["storage"],"targetKind":"guest"}]}"#; fn fingerprint(hex_tail: &str) -> SchemaFingerprint { SchemaFingerprint::parse(format!("sha256:{}{hex_tail}", "0".repeat(63))).unwrap() @@ -2278,6 +2891,29 @@ mod tests { false, ) .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("volume-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) + .unwrap() } fn state_views() -> BTreeMap { @@ -2328,7 +2964,7 @@ mod tests { } fn binding() -> ResourceApiBinding { - ResourceApiBinding::new( + ResourceApiBinding::new_with_placement( ResourceTypeName::parse("Volume").unwrap(), SchemaVersion::new(1, 0).unwrap(), fingerprint("2"), @@ -2341,6 +2977,7 @@ mod tests { .unwrap(), None, None, + PlacementAnchor::ExecutionRef, ) .unwrap() } @@ -2411,6 +3048,21 @@ mod tests { }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + ]) + .unwrap() } fn manifest_with_projection_factory( @@ -2429,7 +3081,23 @@ mod tests { false, ) .unwrap(); - let binding = ResourceApiBinding::new( + let controller = controller + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("semantic-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::ZoneSingleton, + [ControllerTargetKind::Zone], + ) + .unwrap() + .with_target_capabilities([ComponentTargetCapability::new( + ControllerTargetKind::Zone, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Runtime], + ) + .unwrap()]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( service_type, SchemaVersion::new(1, 0).unwrap(), fingerprint("2"), @@ -2438,6 +3106,7 @@ mod tests { StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::Zone, ) .unwrap(); ProviderManifest::new( @@ -2456,6 +3125,244 @@ mod tests { ) } + #[test] + fn controller_placement_contract_round_trips_and_requires_one_instance_scope() { + let component = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [ + ResourceTypeName::parse("Process").unwrap(), + ResourceTypeName::parse("EphemeralProcess").unwrap(), + ], + [BoundedToken::parse("assess-update").unwrap()], + [ExecutionDomain::System], + 32, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + + let bytes = canonical_json_bytes(&component).unwrap(); + let parsed: ComponentDescriptor = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(parsed, component); + assert_eq!( + parsed.instance_scope(), + Some(ControllerInstanceScope::PerResourceTarget) + ); + assert_eq!( + parsed.supported_target_kinds(), + &BTreeSet::from([ControllerTargetKind::Host, ControllerTargetKind::Guest]) + ); + assert_eq!(parsed.target_capabilities().len(), 2); + } + + #[test] + fn placement_anchor_is_registered_and_rejects_incompatible_resource_schema() { + let process = ResourceTypeName::parse("Process").unwrap(); + let valid = ResourceApiBinding::new_with_placement( + process.clone(), + SchemaVersion::new(1, 0).unwrap(), + fingerprint("2"), + SchemaVersion::new(1, 0).unwrap(), + fingerprint("3"), + StandardCapabilityMatrix::default(), + None, + None, + PlacementAnchor::ExecutionRef, + ) + .unwrap(); + assert_eq!(valid.placement_anchor(), Some(&PlacementAnchor::ExecutionRef)); + + assert_eq!( + ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + fingerprint("2"), + SchemaVersion::new(1, 0).unwrap(), + fingerprint("3"), + StandardCapabilityMatrix::default(), + None, + None, + PlacementAnchor::Zone, + ) + .unwrap_err(), + ProviderContractError::PlacementAnchorMismatch + ); + } + + #[test] + fn same_platform_runtime_artifacts_must_match_across_host_and_guest() { + let host = TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(); + let guest = TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_B).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(); + assert_eq!( + ProviderManifest::validate_runtime_artifacts([host, guest]), + Err(ProviderContractError::SharedRuntimeArtifactMismatch) + ); + } + + #[test] + fn same_platform_controller_components_use_one_artifact_digest() { + let mut subject = manifest(); + subject.components[0].target_capabilities[1].artifact_digest = + ArtifactDigest::parse(DIGEST_A).unwrap(); + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ComponentArtifactMismatch) + ); + } + + #[test] + fn zone_singleton_cannot_own_execution_ref_resources() { + let mut subject = manifest(); + let process = ResourceTypeName::parse("Process").unwrap(); + subject.components[0].exported_resource_types = BTreeSet::from([process.clone()]); + subject.api_bindings[0].resource_type = process; + subject.api_bindings[0].placement_anchor = Some(PlacementAnchor::ExecutionRef); + let component = &mut subject.components[0]; + component.instance_scope = Some(ControllerInstanceScope::ZoneSingleton); + component.supported_target_kinds = BTreeSet::from([ControllerTargetKind::Zone]); + component.target_capabilities = vec![ + ComponentTargetCapability::new( + ControllerTargetKind::Zone, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [], + ) + .unwrap(), + ]; + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeInvalid) + ); + } + + #[test] + fn per_resource_target_cannot_own_zone_resources() { + let mut subject = manifest(); + let wayland = + ResourceTypeName::parse("display-wayland.d2bus.org.WaylandSession").unwrap(); + subject.components[0].exported_resource_types = BTreeSet::from([wayland.clone()]); + subject.api_bindings[0].resource_type = wayland; + subject.api_bindings[0].placement_anchor = Some(PlacementAnchor::Zone); + subject.components[0].instance_scope = Some(ControllerInstanceScope::PerResourceTarget); + subject.components[0].supported_target_kinds = + BTreeSet::from([ControllerTargetKind::Host, ControllerTargetKind::Guest]); + subject.components[0].target_capabilities = vec![ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [], + ) + .unwrap(), + ]; + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeInvalid) + ); + } + + #[test] + fn volume_cannot_bypass_controller_scope_admission() { + let mut subject = manifest(); + subject.api_bindings[0].placement_anchor = Some(PlacementAnchor::Zone); + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeInvalid) + ); + } + + #[test] + fn installation_validation_fences_missing_scope_and_runtime_artifacts() { + let mut missing_runtime = manifest(); + missing_runtime.runtime_artifacts.clear(); + assert_eq!( + missing_runtime.validate_installation_contract(), + Err(ProviderContractError::RuntimeArtifactMissing) + ); + + let mut missing_scope = controller(); + missing_scope.instance_scope = None; + missing_scope.supported_target_kinds.clear(); + missing_scope.target_capabilities.clear(); + let invalid = ProviderManifest::new( + ArtifactId::parse("provider-volume-local").unwrap(), + digests(), + trusted(), + compatibility(), + [missing_scope], + [binding()], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + ]) + .unwrap(); + assert_eq!( + invalid.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeMissing) + ); + } + #[test] fn schema_vector_pins_the_minimal_provider_base_spec() { let spec = ProviderSpec::minimal(ArtifactId::parse("provider-wayland").unwrap()); @@ -2511,7 +3418,7 @@ mod tests { let parsed: ComponentDescriptor = serde_json::from_slice(&bytes).unwrap(); assert!(parsed.state_namespaces().is_empty()); assert!(!parsed.declares_state_volume()); - assert_eq!(parsed.execution(), &ComponentExecution::InProcessBootstrap); + assert!(parsed.execution().is_launchable()); } #[test] @@ -2919,6 +3826,24 @@ mod tests { ); } + #[test] + fn component_cardinality_is_bounded_before_installation() { + assert_eq!( + ComponentDescriptor::new( + BoundedToken::parse("bounded-controller").unwrap(), + ComponentType::Controller, + [ResourceTypeName::parse("Volume").unwrap()], + [], + [ExecutionDomain::System], + u32::MAX, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [], + false, + ), + Err(ProviderContractError::BoundExceeded) + ); + } + #[test] fn an_unlisted_optional_capability_is_not_supported() { let matrix = StandardCapabilityMatrix::new([ @@ -3240,6 +4165,30 @@ mod tests { false, ) .unwrap(); + let duplicate_controller = duplicate_controller + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("second-volume-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) + .unwrap(); assert_eq!( ProviderManifest::new( ArtifactId::parse("provider-volume-local").unwrap(), @@ -3254,7 +4203,7 @@ mod tests { Err(ProviderContractError::DuplicateDeclaration) ); - let unowned_binding = ResourceApiBinding::new( + let unowned_binding = ResourceApiBinding::new_with_placement( ResourceTypeName::parse("Network").unwrap(), SchemaVersion::new(1, 0).unwrap(), fingerprint("2"), @@ -3263,6 +4212,7 @@ mod tests { StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::Zone, ) .unwrap(); assert_eq!( diff --git a/packages/d2b-contracts-provider/src/v3/semantic_services/child_resources.rs b/packages/d2b-contracts-provider/src/v3/semantic_services/child_resources.rs new file mode 100644 index 0000000000..1809ec1bbe --- /dev/null +++ b/packages/d2b-contracts-provider/src/v3/semantic_services/child_resources.rs @@ -0,0 +1,942 @@ +//! Deterministic child-resource intents for explicit semantic Bindings. +//! +//! A Binding controller uses this module to describe the Process, +//! EphemeralProcess, and Endpoint resources it owns. The returned intents +//! contain no store-generated UID and are safe to submit through Core's +//! resource admission path. Construction requires an authored Binding, +//! Service, target, and Provider reference; there is deliberately no API that +//! derives a child from a Service alone. + +use std::fmt::Write; + +use sha2::{Digest, Sha256}; + +use super::{BindingTargetType, SemanticFamily}; +use d2b_contracts_resource::v3::{ExecutionDomain, ResourceName, ResourceRef}; + +/// The resource kinds a semantic Binding may own. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BindingChildKind { + /// A long-lived provider component. + Process, + /// A one-shot provider worker. + EphemeralProcess, + /// A stable endpoint produced by a child process. + Endpoint, +} + +impl BindingChildKind { + /// Return the canonical ResourceType name. + pub const fn resource_type(self) -> &'static str { + match self { + Self::Process => "Process", + Self::EphemeralProcess => "EphemeralProcess", + Self::Endpoint => "Endpoint", + } + } + + const fn teardown_rank(self) -> u8 { + match self { + Self::Endpoint => 0, + Self::EphemeralProcess => 1, + Self::Process => 2, + } + } +} + +/// The target on which a child resource is reconciled. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BindingChildPlacement { + /// The host target. + Host, + /// The consuming guest target. + Guest, +} + +/// One controller-declared child shape. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BindingChildRequest { + kind: BindingChildKind, + placement: BindingChildPlacement, + role: &'static str, + producer_role: Option<&'static str>, + process_provider: Option<&'static str>, + process_template: Option<&'static str>, + process_domain: Option, + process_class: Option<&'static str>, + process_user: bool, +} + +impl BindingChildRequest { + /// Construct a child request with no Endpoint producer dependency. + pub const fn new( + kind: BindingChildKind, + placement: BindingChildPlacement, + role: &'static str, + ) -> Self { + Self { + kind, + placement, + role, + producer_role: None, + process_provider: None, + process_template: None, + process_domain: None, + process_class: None, + process_user: false, + } + } + + /// Construct a Process or EphemeralProcess request with its signed + /// execution contract. + pub const fn process( + kind: BindingChildKind, + placement: BindingChildPlacement, + role: &'static str, + provider: &'static str, + template: &'static str, + domain: ExecutionDomain, + class: &'static str, + ) -> Self { + Self { + kind, + placement, + role, + producer_role: None, + process_provider: Some(provider), + process_template: Some(template), + process_domain: Some(domain), + process_class: Some(class), + process_user: false, + } + } + + /// Construct a user-domain Process request whose identity is supplied by + /// the authored Binding target. + pub const fn process_for_user( + kind: BindingChildKind, + placement: BindingChildPlacement, + role: &'static str, + provider: &'static str, + template: &'static str, + class: &'static str, + ) -> Self { + Self { + kind, + placement, + role, + producer_role: None, + process_provider: Some(provider), + process_template: Some(template), + process_domain: Some(ExecutionDomain::User), + process_class: Some(class), + process_user: true, + } + } + + /// Construct an Endpoint request produced by another declared child. + pub const fn endpoint( + placement: BindingChildPlacement, + role: &'static str, + producer_role: &'static str, + ) -> Self { + Self { + kind: BindingChildKind::Endpoint, + placement, + role, + producer_role: Some(producer_role), + process_provider: None, + process_template: None, + process_domain: None, + process_class: None, + process_user: false, + } + } + + /// Return the requested kind. + pub const fn kind(self) -> BindingChildKind { + self.kind + } + + /// Return the requested placement. + pub const fn placement(self) -> BindingChildPlacement { + self.placement + } + + /// Return the Provider-local semantic role. + pub const fn role(self) -> &'static str { + self.role + } + + /// Return the Provider-local producer role, if this is an Endpoint. + pub const fn producer_role(self) -> Option<&'static str> { + self.producer_role + } + + /// Return the Process Provider reference, if explicitly declared. + pub const fn process_provider(self) -> Option<&'static str> { + self.process_provider + } + + /// Return the signed Process template, if explicitly declared. + pub const fn process_template(self) -> Option<&'static str> { + self.process_template + } + + /// Return the signed Process execution domain, if explicitly declared. + pub const fn process_domain(self) -> Option { + self.process_domain + } + + /// Return the signed Process class, if explicitly declared. + pub const fn process_class(self) -> Option<&'static str> { + self.process_class + } + + /// Whether the Process requires the Binding's User identity. + pub const fn process_user(self) -> bool { + self.process_user + } +} + +/// One UID-free child-resource intent owned by a Binding. +#[derive(Clone, PartialEq, Eq)] +pub struct BindingChildIntent { + owner_ref: ResourceRef, + provider_ref: ResourceRef, + resource_ref: ResourceRef, + execution_ref: ResourceRef, + kind: BindingChildKind, + placement: BindingChildPlacement, + role: &'static str, + producer_ref: Option, + process_provider: Option<&'static str>, + process_template: Option<&'static str>, + process_domain: Option, + process_class: Option<&'static str>, + process_user: Option, +} + +impl core::fmt::Debug for BindingChildIntent { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("BindingChildIntent") + .field("kind", &self.kind) + .field("placement", &self.placement) + .field("role", &self.role) + .field("has_producer", &self.producer_ref.is_some()) + .finish_non_exhaustive() + } +} + +impl BindingChildIntent { + /// Borrow the authored Binding owner reference. + pub const fn owner_ref(&self) -> &ResourceRef { + &self.owner_ref + } + + /// Borrow the Provider that owns the child controller. + pub const fn provider_ref(&self) -> &ResourceRef { + &self.provider_ref + } + + /// Borrow the UID-free child reference. + pub const fn resource_ref(&self) -> &ResourceRef { + &self.resource_ref + } + + /// Borrow the Core-resolved execution target for this child. + /// + /// Host children always execute under the canonical Host resource. Guest + /// children execute under the Binding's admitted Guest target. + pub const fn execution_ref(&self) -> &ResourceRef { + &self.execution_ref + } + + /// Return the child kind. + pub const fn kind(&self) -> BindingChildKind { + self.kind + } + + /// Return the resolved target placement. + pub const fn placement(&self) -> BindingChildPlacement { + self.placement + } + + /// Return the Provider-local role. + pub const fn role(&self) -> &'static str { + self.role + } + + /// Borrow the Endpoint producer reference, if this child is an Endpoint. + pub const fn producer_ref(&self) -> Option<&ResourceRef> { + self.producer_ref.as_ref() + } + + /// Return the Process Provider reference, if explicitly declared. + pub const fn process_provider(&self) -> Option<&'static str> { + self.process_provider + } + + /// Return the signed Process template, if explicitly declared. + pub const fn process_template(&self) -> Option<&'static str> { + self.process_template + } + + /// Return the signed Process execution domain, if explicitly declared. + pub const fn process_domain(&self) -> Option { + self.process_domain + } + + /// Return the signed Process class, if explicitly declared. + pub const fn process_class(&self) -> Option<&'static str> { + self.process_class + } + + /// Borrow the Process User identity, if one was admitted. + pub const fn process_user(&self) -> Option<&ResourceRef> { + self.process_user.as_ref() + } +} + +/// An ordered set of children owned by one explicit Binding. +#[derive(Clone, PartialEq, Eq)] +pub struct BindingChildSet { + family: SemanticFamily, + owner_ref: ResourceRef, + target_ref: ResourceRef, + children: Vec, +} + +impl core::fmt::Debug for BindingChildSet { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("BindingChildSet") + .field("family", &self.family) + .field("child_count", &self.children.len()) + .finish_non_exhaustive() + } +} + +impl BindingChildSet { + /// Return the semantic family represented by this set. + pub const fn family(&self) -> SemanticFamily { + self.family + } + + /// Borrow the explicit Binding owner. + pub const fn owner_ref(&self) -> &ResourceRef { + &self.owner_ref + } + + /// Borrow the Binding's admitted consuming target. + pub const fn target_ref(&self) -> &ResourceRef { + &self.target_ref + } + + /// Iterate over children in declaration order. + pub fn iter(&self) -> impl Iterator { + self.children.iter() + } + + /// Return a child by its Provider-local role. + pub fn child(&self, role: &str) -> Option<&BindingChildIntent> { + self.children.iter().find(|child| child.role == role) + } + + /// Iterate over children placed on one target. + pub fn at( + &self, + placement: BindingChildPlacement, + ) -> impl Iterator { + self.children + .iter() + .filter(move |child| child.placement == placement) + } + + /// Return all child references without store-generated identities. + pub fn resource_refs(&self) -> impl Iterator { + self.children.iter().map(BindingChildIntent::resource_ref) + } + + /// Return children in safe deletion order. + /// + /// Endpoints are deleted before their producing Process resources. The + /// original declaration order is retained for children with the same + /// teardown rank. + pub fn teardown_order(&self) -> Vec<&BindingChildIntent> { + let mut children = self.children.iter().collect::>(); + children.sort_by_key(|child| child.kind.teardown_rank()); + children + } + + /// Whether the set contains a child of the supplied kind. + pub fn contains_kind(&self, kind: BindingChildKind) -> bool { + self.children.iter().any(|child| child.kind == kind) + } +} + +/// Closed errors while constructing child intents. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BindingChildError { + /// The Binding reference did not name this family's Binding ResourceType. + InvalidBindingRef, + /// The Service reference did not name this family's Service ResourceType. + InvalidServiceRef, + /// The target reference did not match the admitted target type. + InvalidTargetRef, + /// The Provider reference did not name a Provider. + InvalidProviderRef, + /// No child declarations were supplied. + EmptyDeclaration, + /// A role was malformed or repeated. + InvalidRole, + /// An Endpoint named a missing producer role. + MissingProducer, + /// A deterministic child reference could not be constructed. + InvalidChildRef, + /// A child placement did not match the Binding's admitted target. + InvalidPlacement, + /// An Endpoint producer role did not name a Process child. + InvalidProducer, + /// A user-domain Process requires an authored User reference. + MissingUser, + /// A supplied User reference did not name a User resource. + InvalidUserRef, +} + +impl core::fmt::Display for BindingChildError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(match self { + Self::InvalidBindingRef => "binding-child-binding-ref-invalid", + Self::InvalidServiceRef => "binding-child-service-ref-invalid", + Self::InvalidTargetRef => "binding-child-target-ref-invalid", + Self::InvalidProviderRef => "binding-child-provider-ref-invalid", + Self::EmptyDeclaration => "binding-child-declaration-empty", + Self::InvalidRole => "binding-child-role-invalid", + Self::MissingProducer => "binding-child-producer-missing", + Self::InvalidChildRef => "binding-child-ref-invalid", + Self::InvalidPlacement => "binding-child-placement-invalid", + Self::InvalidProducer => "binding-child-producer-invalid", + Self::MissingUser => "binding-child-user-missing", + Self::InvalidUserRef => "binding-child-user-ref-invalid", + }) + } +} + +impl std::error::Error for BindingChildError {} + +/// Construct child intents for one explicitly authored semantic Binding. +/// +/// This is the only constructor for a [`BindingChildSet`]. In particular, +/// callers must provide the Binding and Service references; a Ready Service +/// alone cannot create consumer children. +pub fn explicit_binding_children( + family: SemanticFamily, + binding_ref: ResourceRef, + service_ref: ResourceRef, + target_ref: ResourceRef, + provider_ref: ResourceRef, + declarations: &[BindingChildRequest], +) -> Result { + explicit_binding_children_with_user( + family, + binding_ref, + service_ref, + target_ref, + provider_ref, + None, + declarations, + ) +} + +/// Construct child intents while supplying the Binding's admitted User +/// identity for user-domain Processes. +pub fn explicit_binding_children_with_user( + family: SemanticFamily, + binding_ref: ResourceRef, + service_ref: ResourceRef, + target_ref: ResourceRef, + provider_ref: ResourceRef, + user_ref: Option, + declarations: &[BindingChildRequest], +) -> Result { + let contract = family.contract(); + if binding_ref.resource_type() != contract.binding().resource_type() { + return Err(BindingChildError::InvalidBindingRef); + } + if service_ref.resource_type() != contract.service().resource_type() { + return Err(BindingChildError::InvalidServiceRef); + } + if provider_ref.resource_type().as_str() != "Provider" { + return Err(BindingChildError::InvalidProviderRef); + } + if let Some(user_ref) = &user_ref + && user_ref.resource_type().as_str() != "User" + { + return Err(BindingChildError::InvalidUserRef); + } + let target = binding_target_type(&target_ref)?; + contract + .admit_binding_refs(&service_ref, target) + .map_err(|_| BindingChildError::InvalidTargetRef)?; + if declarations.is_empty() { + return Err(BindingChildError::EmptyDeclaration); + } + if declarations.iter().any(|declaration| { + matches!(declaration.placement, BindingChildPlacement::Guest) + && target != BindingTargetType::Guest + }) { + return Err(BindingChildError::InvalidPlacement); + } + + let mut roles = Vec::with_capacity(declarations.len()); + let mut refs = Vec::with_capacity(declarations.len()); + for declaration in declarations { + if !valid_role(declaration.role) || roles.contains(&declaration.role) { + return Err(BindingChildError::InvalidRole); + } + if declaration.producer_role.is_some() && declaration.kind != BindingChildKind::Endpoint { + return Err(BindingChildError::InvalidProducer); + } + if declaration.kind == BindingChildKind::Endpoint + && (declaration.process_provider.is_some() + || declaration.process_template.is_some() + || declaration.process_domain.is_some() + || declaration.process_class.is_some() + || declaration.process_user) + { + return Err(BindingChildError::InvalidProducer); + } + if declaration.kind != BindingChildKind::Endpoint { + if let Some(provider) = declaration.process_provider { + let provider_ref = ResourceRef::parse(provider) + .map_err(|_| BindingChildError::InvalidProviderRef)?; + if provider_ref.resource_type().as_str() != "Provider" { + return Err(BindingChildError::InvalidProviderRef); + } + } + if let Some(template) = declaration.process_template { + if !valid_role(template) { + return Err(BindingChildError::InvalidRole); + } + } + if let Some(class) = declaration.process_class { + if !matches!(class, "controller" | "service" | "worker") { + return Err(BindingChildError::InvalidRole); + } + } + if declaration.process_user && user_ref.is_none() { + return Err(BindingChildError::MissingUser); + } + } + roles.push(declaration.role); + let name = child_name(family, &binding_ref, declaration); + let resource_ref = ResourceRef::parse(&format!( + "{}/{}", + declaration.kind.resource_type(), + name.as_str() + )) + .map_err(|_| BindingChildError::InvalidChildRef)?; + refs.push(resource_ref); + } + + let mut children = Vec::with_capacity(declarations.len()); + for (declaration, resource_ref) in declarations.iter().zip(refs.iter().cloned()) { + if declaration.kind == BindingChildKind::Endpoint && declaration.producer_role.is_none() { + return Err(BindingChildError::MissingProducer); + } + let producer_ref = declaration + .producer_role + .map(|role| refs_for_role(declarations, &refs, role)); + if let Some(producer_role) = declaration.producer_role { + let producer = declarations + .iter() + .find(|candidate| candidate.role == producer_role) + .ok_or(BindingChildError::MissingProducer)?; + if !matches!( + producer.kind, + BindingChildKind::Process | BindingChildKind::EphemeralProcess + ) { + return Err(BindingChildError::InvalidProducer); + } + if producer.placement != declaration.placement { + return Err(BindingChildError::InvalidPlacement); + } + } + let execution_ref = match declaration.placement { + BindingChildPlacement::Host => { + ResourceRef::parse("Host/host-system").expect("canonical Host reference") + } + BindingChildPlacement::Guest => target_ref.clone(), + }; + let Some(producer_ref) = producer_ref.transpose()? else { + children.push(BindingChildIntent { + owner_ref: binding_ref.clone(), + provider_ref: provider_ref.clone(), + resource_ref, + execution_ref, + kind: declaration.kind, + placement: declaration.placement, + role: declaration.role, + producer_ref: None, + process_provider: declaration.process_provider, + process_template: declaration.process_template, + process_domain: declaration.process_domain, + process_class: declaration.process_class, + process_user: if declaration.process_user { + user_ref.clone() + } else { + None + }, + }); + continue; + }; + children.push(BindingChildIntent { + owner_ref: binding_ref.clone(), + provider_ref: provider_ref.clone(), + resource_ref, + execution_ref, + kind: declaration.kind, + placement: declaration.placement, + role: declaration.role, + producer_ref: Some(producer_ref), + process_provider: declaration.process_provider, + process_template: declaration.process_template, + process_domain: declaration.process_domain, + process_class: declaration.process_class, + process_user: if declaration.process_user { + user_ref.clone() + } else { + None + }, + }); + } + Ok(BindingChildSet { + family, + owner_ref: binding_ref, + target_ref, + children, + }) +} + +fn refs_for_role( + declarations: &[BindingChildRequest], + refs: &[ResourceRef], + role: &'static str, +) -> Result { + let index = declarations + .iter() + .position(|declaration| declaration.role == role) + .ok_or(BindingChildError::MissingProducer)?; + refs.get(index) + .cloned() + .ok_or(BindingChildError::InvalidChildRef) +} + +fn binding_target_type(target_ref: &ResourceRef) -> Result { + match target_ref.resource_type().as_str() { + "Guest" => Ok(BindingTargetType::Guest), + "User" => Ok(BindingTargetType::User), + "Zone" => Ok(BindingTargetType::Zone), + _ => Err(BindingChildError::InvalidTargetRef), + } +} + +fn valid_role(role: &str) -> bool { + let bytes = role.as_bytes(); + !bytes.is_empty() + && bytes.len() <= 24 + && bytes.last() != Some(&b'-') + && bytes.iter().enumerate().all(|(index, byte)| { + byte.is_ascii_lowercase() + || byte.is_ascii_digit() + || (*byte == b'-' && index > 0 && bytes[index - 1] != b'-') + }) +} + +fn child_name( + family: SemanticFamily, + binding_ref: &ResourceRef, + declaration: &BindingChildRequest, +) -> ResourceName { + let mut input = String::with_capacity(128); + input.push_str(family.namespace()); + input.push('\0'); + input.push_str(&binding_ref.to_canonical_string()); + input.push('\0'); + input.push_str(declaration.kind.resource_type()); + input.push('\0'); + input.push_str(declaration.role); + let digest = Sha256::digest(input.as_bytes()); + let mut short_digest = String::with_capacity(12); + for byte in &digest[..6] { + write!(&mut short_digest, "{byte:02x}").expect("writing to String cannot fail"); + } + let candidate = format!( + "{}-{short_digest}-{}", + family_slug(family), + declaration.role + ); + ResourceName::parse(candidate).expect("child name is bounded by closed constants") +} + +const fn family_slug(family: SemanticFamily) -> &'static str { + match family { + SemanticFamily::Audio => "audio", + SemanticFamily::SecurityKey => "security-key", + SemanticFamily::Telemetry => "telemetry", + SemanticFamily::Usb => "usb", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn refs() -> (ResourceRef, ResourceRef, ResourceRef, ResourceRef) { + ( + ResourceRef::parse("audio.d2bus.org.AudioBinding/mic").unwrap(), + ResourceRef::parse("audio.d2bus.org.AudioService/host").unwrap(), + ResourceRef::parse("Guest/dev-vm").unwrap(), + ResourceRef::parse("Provider/audio-pipewire").unwrap(), + ) + } + + #[test] + fn explicit_binding_children_are_deterministic_and_uid_free() { + let (binding, service, target, provider) = refs(); + let declarations = [ + BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Host, + "host-effect", + ), + BindingChildRequest::endpoint( + BindingChildPlacement::Host, + "host-endpoint", + "host-effect", + ), + BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Guest, + "guest-agent", + ), + BindingChildRequest::endpoint( + BindingChildPlacement::Guest, + "guest-endpoint", + "guest-agent", + ), + ]; + let first = explicit_binding_children( + SemanticFamily::Audio, + binding.clone(), + service.clone(), + target.clone(), + provider.clone(), + &declarations, + ) + .unwrap(); + let second = explicit_binding_children( + SemanticFamily::Audio, + binding, + service, + target, + provider, + &declarations, + ) + .unwrap(); + assert_eq!(first, second); + assert_eq!(first.iter().count(), 4); + assert_eq!( + first.child("host-effect").unwrap().execution_ref(), + &ResourceRef::parse("Host/host-system").unwrap() + ); + assert_eq!( + first.child("guest-agent").unwrap().execution_ref(), + &ResourceRef::parse("Guest/dev-vm").unwrap() + ); + assert_eq!( + first.target_ref(), + &ResourceRef::parse("Guest/dev-vm").unwrap() + ); + assert_eq!( + first + .child("host-endpoint") + .unwrap() + .producer_ref() + .unwrap() + .resource_type() + .as_str(), + "Process" + ); + assert_eq!( + first + .teardown_order() + .iter() + .take(2) + .map(|child| child.kind()) + .collect::>(), + vec![BindingChildKind::Endpoint, BindingChildKind::Endpoint] + ); + } + + #[test] + fn service_alone_cannot_create_children() { + let (_, service, target, provider) = refs(); + let result = explicit_binding_children( + SemanticFamily::Audio, + ResourceRef::parse("audio.d2bus.org.AudioService/host").unwrap(), + service, + target, + provider, + &[BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Host, + "host-effect", + )], + ); + assert_eq!(result, Err(BindingChildError::InvalidBindingRef)); + } + + #[test] + fn wrong_service_target_and_producer_are_rejected() { + let (binding, service, _, provider) = refs(); + let declaration = [BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Guest, + "guest-agent", + )]; + assert_eq!( + explicit_binding_children( + SemanticFamily::Audio, + binding.clone(), + ResourceRef::parse("usb.d2bus.org.UsbService/usb").unwrap(), + ResourceRef::parse("Guest/dev-vm").unwrap(), + provider.clone(), + &declaration, + ), + Err(BindingChildError::InvalidServiceRef) + ); + assert_eq!( + explicit_binding_children( + SemanticFamily::Audio, + binding.clone(), + service.clone(), + ResourceRef::parse("Zone/dev").unwrap(), + provider.clone(), + &declaration, + ), + Err(BindingChildError::InvalidTargetRef) + ); + assert_eq!( + explicit_binding_children( + SemanticFamily::Audio, + binding.clone(), + service.clone(), + ResourceRef::parse("Guest/dev-vm").unwrap(), + provider.clone(), + &[BindingChildRequest::endpoint( + BindingChildPlacement::Guest, + "guest-endpoint", + "missing", + )], + ), + Err(BindingChildError::MissingProducer) + ); + assert_eq!( + explicit_binding_children( + SemanticFamily::Telemetry, + ResourceRef::parse("telemetry.d2bus.org.TelemetryBinding/metrics").unwrap(), + ResourceRef::parse("telemetry.d2bus.org.TelemetryService/host").unwrap(), + ResourceRef::parse("Zone/dev").unwrap(), + ResourceRef::parse("Provider/observability-otel").unwrap(), + &[BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Guest, + "guest-agent", + )], + ), + Err(BindingChildError::InvalidPlacement) + ); + assert_eq!( + explicit_binding_children( + SemanticFamily::Audio, + ResourceRef::parse("audio.d2bus.org.AudioBinding/mic").unwrap(), + ResourceRef::parse("audio.d2bus.org.AudioService/host").unwrap(), + ResourceRef::parse("Guest/dev-vm").unwrap(), + ResourceRef::parse("Provider/audio-pipewire").unwrap(), + &[ + BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Host, + "host-effect", + ), + BindingChildRequest::endpoint( + BindingChildPlacement::Host, + "host-endpoint", + "host-endpoint", + ), + ], + ), + Err(BindingChildError::InvalidProducer) + ); + assert_eq!( + explicit_binding_children( + SemanticFamily::Audio, + ResourceRef::parse("audio.d2bus.org.AudioBinding/mic").unwrap(), + ResourceRef::parse("audio.d2bus.org.AudioService/host").unwrap(), + ResourceRef::parse("Guest/dev-vm").unwrap(), + ResourceRef::parse("Provider/audio-pipewire").unwrap(), + &[BindingChildRequest::new( + BindingChildKind::Endpoint, + BindingChildPlacement::Guest, + "guest-endpoint", + )], + ), + Err(BindingChildError::MissingProducer) + ); + assert_eq!( + explicit_binding_children( + SemanticFamily::Audio, + binding, + service, + ResourceRef::parse("Guest/dev-vm").unwrap(), + ResourceRef::parse("Provider/audio-pipewire").unwrap(), + &[ + BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Host, + "host-effect", + ), + BindingChildRequest::endpoint( + BindingChildPlacement::Guest, + "guest-endpoint", + "host-effect", + ), + ], + ), + Err(BindingChildError::InvalidPlacement) + ); + } + + #[test] + fn malformed_roles_are_rejected_without_panicking() { + let (binding, service, target, provider) = refs(); + assert_eq!( + explicit_binding_children( + SemanticFamily::Audio, + binding, + service, + target, + provider, + &[BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Guest, + "guest-agent-", + )], + ), + Err(BindingChildError::InvalidRole) + ); + } +} diff --git a/packages/d2b-contracts-provider/src/v3/semantic_services/mod.rs b/packages/d2b-contracts-provider/src/v3/semantic_services/mod.rs index 386841d8a8..f0b849958e 100644 --- a/packages/d2b-contracts-provider/src/v3/semantic_services/mod.rs +++ b/packages/d2b-contracts-provider/src/v3/semantic_services/mod.rs @@ -51,6 +51,7 @@ use d2b_contracts_resource::v3::{ }; pub mod audio; +pub mod child_resources; pub mod security_key; pub mod telemetry; pub mod usb; diff --git a/packages/d2b-contracts-provider/src/v3/telemetry_policy.rs b/packages/d2b-contracts-provider/src/v3/telemetry_policy.rs index 2c110a8fe6..09dbc6d786 100644 --- a/packages/d2b-contracts-provider/src/v3/telemetry_policy.rs +++ b/packages/d2b-contracts-provider/src/v3/telemetry_policy.rs @@ -188,7 +188,7 @@ pub const METRIC_LABEL_POLICY: &[(&str, &[&str])] = &[ "local-vm", "qemu-media", "unsafe-local", - "guest-control", + "component-session", ], ), ("domain", &["system", "user"]), diff --git a/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto b/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto index e5427a80dc..446b4289ad 100644 --- a/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto +++ b/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto @@ -213,6 +213,9 @@ message DeleteResponse { message CommitBatchRequest { RequestMeta meta = 1; repeated Mutation mutations = 2; + // Bus-admitted assignment evidence attached to the existing CommitBatch + // transport. This field is populated only after ResourceCall admission. + bytes scoped_admission = 3; } message CommitBatchResponse { diff --git a/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs b/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs index 5713ae0669..9b1cbe3abf 100644 --- a/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs +++ b/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs @@ -5166,6 +5166,8 @@ pub struct CommitBatchRequest { pub meta: ::protobuf::MessageField, // @@protoc_insertion_point(field:d2b.resource.v3.CommitBatchRequest.mutations) pub mutations: ::std::vec::Vec, + // @@protoc_insertion_point(field:d2b.resource.v3.CommitBatchRequest.scoped_admission) + pub scoped_admission: ::std::vec::Vec, // special fields // @@protoc_insertion_point(special_field:d2b.resource.v3.CommitBatchRequest.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -5183,7 +5185,7 @@ impl CommitBatchRequest { } fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); + let mut fields = ::std::vec::Vec::with_capacity(3); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMeta>( "meta", @@ -5195,6 +5197,11 @@ impl CommitBatchRequest { |m: &CommitBatchRequest| { &m.mutations }, |m: &mut CommitBatchRequest| { &mut m.mutations }, )); + fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( + "scoped_admission", + |m: &CommitBatchRequest| { &m.scoped_admission }, + |m: &mut CommitBatchRequest| { &mut m.scoped_admission }, + )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "CommitBatchRequest", fields, @@ -5219,6 +5226,9 @@ impl ::protobuf::Message for CommitBatchRequest { 18 => { self.mutations.push(is.read_message()?); }, + 26 => { + self.scoped_admission = is.read_bytes()?; + }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; }, @@ -5239,6 +5249,9 @@ impl ::protobuf::Message for CommitBatchRequest { let len = value.compute_size(); my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; }; + if !self.scoped_admission.is_empty() { + my_size += ::protobuf::rt::bytes_size(3, &self.scoped_admission); + } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); my_size @@ -5251,6 +5264,9 @@ impl ::protobuf::Message for CommitBatchRequest { for v in &self.mutations { ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; }; + if !self.scoped_admission.is_empty() { + os.write_bytes(3, &self.scoped_admission)?; + } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) } @@ -5270,6 +5286,7 @@ impl ::protobuf::Message for CommitBatchRequest { fn clear(&mut self) { self.meta.clear(); self.mutations.clear(); + self.scoped_admission.clear(); self.special_fields.clear(); } @@ -5277,6 +5294,7 @@ impl ::protobuf::Message for CommitBatchRequest { static instance: CommitBatchRequest = CommitBatchRequest { meta: ::protobuf::MessageField::none(), mutations: ::std::vec::Vec::new(), + scoped_admission: ::std::vec::Vec::new(), special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -7406,21 +7424,22 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \x08resource\x12\x1a\n\x08revision\x18\x02\x20\x01(\x04R\x08revision\x12\ 4\n\x05error\x18\x03\x20\x01(\x0b2\x1e.d2b.resource.v3.ResourceErrorR\ \x05error\x12G\n\x0bdisposition\x18\x04\x20\x01(\x0e2%.d2b.resource.v3.R\ - econcileDispositionR\x0bdisposition\"\x7f\n\x12CommitBatchRequest\x120\n\ - \x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resource.v3.RequestMetaR\x04meta\ - \x127\n\tmutations\x18\x02\x20\x03(\x0b2\x19.d2b.resource.v3.MutationR\t\ - mutations\"\x8c\x02\n\x13CommitBatchResponse\x12D\n\tresources\x18\x01\ - \x20\x03(\x0b2&.d2b.resource.v3.ResourceEnvelopeBytesR\tresources\x12\ - \x1a\n\x08revision\x18\x02\x20\x01(\x04R\x08revision\x124\n\x05error\x18\ - \x03\x20\x01(\x0b2\x1e.d2b.resource.v3.ResourceErrorR\x05error\x12?\n\ - \x19conflict_mutation_ordinal\x18\x04\x20\x01(\rH\0R\x17conflictMutation\ - Ordinal\x88\x01\x01B\x1c\n\x1a_conflict_mutation_ordinal\"\x80\x01\n\x11\ - ResolveRefRequest\x120\n\x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resource.\ - v3.RequestMetaR\x04meta\x129\n\x06target\x18\x02\x20\x01(\x0b2!.d2b.reso\ - urce.v3.ResourceIdentityR\x06target\"\x89\x01\n\x12ResolveRefResponse\ - \x12=\n\x08resource\x18\x01\x20\x01(\x0b2!.d2b.resource.v3.ResourceIdent\ - ityR\x08resource\x124\n\x05error\x18\x02\x20\x01(\x0b2\x1e.d2b.resource.\ - v3.ResourceErrorR\x05error\"m\n\x14InspectSchemaRequest\x120\n\x04meta\ + econcileDispositionR\x0bdisposition\"\xaa\x01\n\x12CommitBatchRequest\ + \x120\n\x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resource.v3.RequestMetaR\ + \x04meta\x127\n\tmutations\x18\x02\x20\x03(\x0b2\x19.d2b.resource.v3.Mut\ + ationR\tmutations\x12)\n\x10scoped_admission\x18\x03\x20\x01(\x0cR\x0fsc\ + opedAdmission\"\x8c\x02\n\x13CommitBatchResponse\x12D\n\tresources\x18\ + \x01\x20\x03(\x0b2&.d2b.resource.v3.ResourceEnvelopeBytesR\tresources\ + \x12\x1a\n\x08revision\x18\x02\x20\x01(\x04R\x08revision\x124\n\x05error\ + \x18\x03\x20\x01(\x0b2\x1e.d2b.resource.v3.ResourceErrorR\x05error\x12?\ + \n\x19conflict_mutation_ordinal\x18\x04\x20\x01(\rH\0R\x17conflictMutati\ + onOrdinal\x88\x01\x01B\x1c\n\x1a_conflict_mutation_ordinal\"\x80\x01\n\ + \x11ResolveRefRequest\x120\n\x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resou\ + rce.v3.RequestMetaR\x04meta\x129\n\x06target\x18\x02\x20\x01(\x0b2!.d2b.\ + resource.v3.ResourceIdentityR\x06target\"\x89\x01\n\x12ResolveRefRespons\ + e\x12=\n\x08resource\x18\x01\x20\x01(\x0b2!.d2b.resource.v3.ResourceIden\ + tityR\x08resource\x124\n\x05error\x18\x02\x20\x01(\x0b2\x1e.d2b.resource\ + .v3.ResourceErrorR\x05error\"m\n\x14InspectSchemaRequest\x120\n\x04meta\ \x18\x01\x20\x01(\x0b2\x1c.d2b.resource.v3.RequestMetaR\x04meta\x12#\n\r\ resource_type\x18\x02\x20\x01(\tR\x0cresourceType\"\x8d\x01\n\x15Inspect\ SchemaResponse\x12>\n\x06schema\x18\x01\x20\x01(\x0b2&.d2b.resource.v3.R\ diff --git a/packages/d2b-contracts-resource/src/v3/activation_nixos.rs b/packages/d2b-contracts-resource/src/v3/activation_nixos.rs index aacf865971..26ef5e1ad0 100644 --- a/packages/d2b-contracts-resource/src/v3/activation_nixos.rs +++ b/packages/d2b-contracts-resource/src/v3/activation_nixos.rs @@ -1,6 +1,10 @@ //! Contracts for the activation-nixos Provider. -use schemars::JsonSchema; +use schemars::{ + JsonSchema, + r#gen::SchemaGenerator, + schema::{Schema, SchemaObject}, +}; use serde::{Deserialize, Deserializer, Serialize}; use super::{ArtifactId, ResourceRef, ResourceTypeName, execution_policy::require_execution_ref}; @@ -9,6 +13,8 @@ use super::{ArtifactId, ResourceRef, ResourceTypeName, execution_policy::require pub const NIXOS_GENERATION_RESOURCE_TYPE: &str = "activation-nixos.d2bus.org.NixosGeneration"; /// The only Provider admitted by the activation generation schema. pub const ACTIVATION_PROVIDER_REF: &str = "Provider/activation-nixos"; +/// Maximum serialized stdin envelope delivered to the activation helper. +pub const MAX_ACTIVATION_RUNNER_INPUT_BYTES: usize = 2048; /// Target-local activation mode. #[derive( @@ -26,6 +32,65 @@ pub enum ActivationMode { Adopt, } +/// Closed input delivered to an activation-runner `EphemeralProcess`. +/// +/// The broker serializes this value into the bounded stdin envelope consumed +/// by `d2b-activation-helper`. It deliberately carries no executable, +/// store path, target path, or caller-controlled bytes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ActivationRunnerInput { + /// Private-catalog artifact identifier. + pub system_artifact_id: ArtifactId, + /// Target generation ordinal bound to the owning `NixosGeneration`. + #[schemars(schema_with = "nonzero_u64_schema")] + pub target_generation: u64, + /// Closed activation mode. + pub activation_mode: ActivationMode, +} + +fn nonzero_u64_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema: SchemaObject = ::json_schema(generator).into(); + schema + .number + .get_or_insert_with(Default::default) + .minimum = Some(1.0); + schema.into() +} + +/// Validation failure for a typed activation-runner input. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ActivationRunnerInputError { + /// Target generation ordinal was zero. + GenerationInvalid, +} + +impl core::fmt::Display for ActivationRunnerInputError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("activation-runner-generation-invalid") + } +} + +impl std::error::Error for ActivationRunnerInputError {} + +impl ActivationRunnerInput { + /// Construct and validate one runner input. + pub fn new( + system_artifact_id: ArtifactId, + target_generation: u64, + activation_mode: ActivationMode, + ) -> Result { + if target_generation == 0 { + return Err(ActivationRunnerInputError::GenerationInvalid); + } + Ok(Self { + system_artifact_id, + target_generation, + activation_mode, + }) + } +} + /// Provider-specific progress detail. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, @@ -92,6 +157,8 @@ pub enum NixosGenerationSpecError { PriorGenerationRefInvalid, /// Artifact identifier was invalid or empty. ArtifactIdInvalid, + /// Target generation ordinal was zero. + GenerationInvalid, } impl core::fmt::Display for NixosGenerationSpecError { @@ -101,6 +168,7 @@ impl core::fmt::Display for NixosGenerationSpecError { Self::ExecutionRefInvalid => "activation-execution-ref-invalid", Self::PriorGenerationRefInvalid => "activation-prior-generation-ref-invalid", Self::ArtifactIdInvalid => "activation-artifact-id-invalid", + Self::GenerationInvalid => "activation-generation-invalid", }) } } diff --git a/packages/d2b-contracts-resource/src/v3/process.rs b/packages/d2b-contracts-resource/src/v3/process.rs index cd031bbd54..5981c69b79 100644 --- a/packages/d2b-contracts-resource/src/v3/process.rs +++ b/packages/d2b-contracts-resource/src/v3/process.rs @@ -18,7 +18,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; use super::{ - ResourceRef, + ActivationRunnerInput, ResourceRef, execution_policy::{ BoundedToken, BudgetSpec, DurationMs, ExecutionDomain, PrimitiveSpecError, redacted_debug, require_execution_ref, require_resource_type, @@ -1383,6 +1383,8 @@ impl<'de> Deserialize<'de> for ProcessSpec { pub struct EphemeralProcessSpec { #[serde(flatten)] execution: ExecutionSpec, + #[serde(skip_serializing_if = "Option::is_none")] + activation_input: Option, start_deadline: DurationMs, runtime_deadline: DurationMs, successful_ttl: DurationMs, @@ -1409,6 +1411,7 @@ impl EphemeralProcessSpec { check_duration(&failed_ttl, 0, 30 * 86_400_000)?; Ok(Self { execution, + activation_input: None, start_deadline, runtime_deadline, successful_ttl, @@ -1434,6 +1437,28 @@ impl EphemeralProcessSpec { &self.execution } + /// Attach the only typed stdin input admitted to an activation runner. + pub fn with_activation_input( + mut self, + input: ActivationRunnerInput, + ) -> Result { + if self.execution.template().as_str() != "activation-nixos-runner" + || !matches!( + self.execution.execution_ref().resource_type().as_str(), + "Host" | "Guest" + ) + { + return Err(PrimitiveSpecError::ConflictingFields); + } + self.activation_input = Some(input); + Ok(self) + } + + /// Borrow the typed activation-runner stdin input, when present. + pub const fn activation_input(&self) -> Option<&ActivationRunnerInput> { + self.activation_input.as_ref() + } + /// Borrow the successful terminal retention. pub const fn successful_ttl(&self) -> &DurationMs { &self.successful_ttl @@ -1491,6 +1516,8 @@ impl<'de> Deserialize<'de> for EphemeralProcessSpec { #[serde(default)] telemetry: TelemetrySpec, #[serde(default)] + activation_input: Option, + #[serde(default)] start_deadline: Option, #[serde(default)] runtime_deadline: Option, @@ -1519,7 +1546,7 @@ impl<'de> Deserialize<'de> for EphemeralProcessSpec { } .into_execution() .map_err(serde::de::Error::custom)?; - Self::new( + let spec = Self::new( execution, wire.start_deadline .unwrap_or_else(|| duration("60s", 1_000, 3_600_000)), @@ -1531,7 +1558,13 @@ impl<'de> Deserialize<'de> for EphemeralProcessSpec { .unwrap_or_else(|| duration("24h", 0, 30 * 86_400_000)), wire.incident_hold, ) - .map_err(serde::de::Error::custom) + .map_err(serde::de::Error::custom)?; + match wire.activation_input { + Some(input) => spec + .with_activation_input(input) + .map_err(serde::de::Error::custom), + None => Ok(spec), + } } } diff --git a/packages/d2b-contracts-resource/src/v3/resource.rs b/packages/d2b-contracts-resource/src/v3/resource.rs index e9c6153d71..c9a97fa926 100644 --- a/packages/d2b-contracts-resource/src/v3/resource.rs +++ b/packages/d2b-contracts-resource/src/v3/resource.rs @@ -527,6 +527,23 @@ impl ResourceSpec { &self.base } + /// Render the type-specific base with the selected Provider reference. + /// + /// Most ResourceType contracts keep `providerRef` in the universal + /// desired-state layer. EndpointSpec also validates that field as part + /// of its typed contract, so callers validating an Endpoint base use this + /// view to reconstruct the complete typed object. + pub fn base_with_provider_ref(&self) -> CanonicalJsonObject { + let mut fields = self.base.clone().into_inner(); + if let Some(provider_ref) = &self.provider_ref { + fields.insert( + "providerRef".to_owned(), + CanonicalJsonValue::String(provider_ref.to_canonical_string()), + ); + } + CanonicalJsonObject::from_inner(fields) + } + /// Borrow the optional Provider extension. pub fn provider(&self) -> Option<&ProviderSpecExtension> { self.provider.as_ref() diff --git a/packages/d2b-contracts-resource/src/v3/resource_schema.rs b/packages/d2b-contracts-resource/src/v3/resource_schema.rs index 1bae74e45e..08fa6a22e4 100644 --- a/packages/d2b-contracts-resource/src/v3/resource_schema.rs +++ b/packages/d2b-contracts-resource/src/v3/resource_schema.rs @@ -16,7 +16,7 @@ use sha2::{Digest, Sha256}; use unicode_normalization::UnicodeNormalization; use super::{ - ResourceEnvelope, ResourceName, ResourceRef, ResourceTypeName, SchemaFingerprint, + ResourceEnvelope, ResourceName, ResourceRef, ResourceTypeName, SchemaFingerprint, ZoneId, resource_status::ProviderStatusExtension, }; @@ -666,6 +666,134 @@ impl JsonSchema for SchemaVersion { } } +/// The closed set of registered ResourceType placement anchors. +/// +/// A placement anchor is a contract-owned selector, not a Provider-defined +/// field path. `Zone` resolves the containing Zone, while `ExecutionRef` +/// resolves the canonical `spec.executionRef` field to one Host or Guest. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "kebab-case")] +pub enum PlacementAnchor { + /// Place the resource at its containing Zone. + Zone, + /// Place the resource at its canonical `spec.executionRef` target. + ExecutionRef, +} + +impl PlacementAnchor { + /// Return the frozen anchor for a ResourceType whose base contract names + /// one. + pub fn canonical_for(resource_type: &ResourceTypeName) -> Option { + match resource_type.as_str() { + "Process" + | "EphemeralProcess" + | "activation-nixos.d2bus.org.NixosGeneration" + | "shell-terminal.d2bus.org.ShellSession" => Some(Self::ExecutionRef), + "display-wayland.d2bus.org.WaylandSession" => Some(Self::Zone), + _ => None, + } + } + + /// Reject an anchor that disagrees with a registered ResourceType base + /// contract. + pub fn validate_for(self, resource_type: &ResourceTypeName) -> Result<(), ResourceSchemaError> { + if Self::canonical_for(resource_type).is_some_and(|expected| expected != self) { + return Err(ResourceSchemaError::PlacementAnchorMismatch); + } + Ok(()) + } + + /// Resolve this anchor against one committed resource envelope. + pub fn resolve( + self, + envelope: &ResourceEnvelope, + ) -> Result { + match self { + Self::Zone => Ok(PlacementTarget::Zone(envelope.metadata().zone().clone())), + Self::ExecutionRef => { + let value = envelope + .spec() + .base() + .get("executionRef") + .ok_or(ResourceSchemaError::PlacementTargetMissing)?; + let CanonicalJsonValue::String(value) = value else { + return Err(ResourceSchemaError::PlacementTargetInvalid); + }; + let reference = ResourceRef::parse(value) + .map_err(|_| ResourceSchemaError::PlacementTargetInvalid)?; + let target_kind = match reference.resource_type().as_str() { + "Host" => PlacementTargetKind::Host, + "Guest" => PlacementTargetKind::Guest, + _ => return Err(ResourceSchemaError::PlacementTargetWrongType), + }; + Ok(PlacementTarget::Execution { + kind: target_kind, + reference, + }) + } + } + } +} + +/// The target kind resolved by an execution-reference placement anchor. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "lowercase")] +pub enum PlacementTargetKind { + /// The physical Host target. + Host, + /// A workload or nested Guest target. + Guest, +} + +/// The resolved value of a registered placement anchor. +#[derive(Clone, PartialEq, Eq)] +pub enum PlacementTarget { + /// The resource is reconciled by the Zone singleton. + Zone(ZoneId), + /// The resource is reconciled at one exact Host or Guest. + Execution { + /// The resolved target kind. + kind: PlacementTargetKind, + /// The exact execution target reference. + reference: ResourceRef, + }, +} + +impl core::fmt::Debug for PlacementTarget { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Zone(_) => formatter.write_str("PlacementTarget::Zone()"), + Self::Execution { kind, .. } => formatter + .debug_struct("PlacementTarget::Execution") + .field("kind", kind) + .finish_non_exhaustive(), + } + } +} + +impl PlacementTarget { + /// Return the target kind for an execution placement, or `None` for the + /// Zone singleton. + pub const fn execution_kind(&self) -> Option { + match self { + Self::Zone(_) => None, + Self::Execution { kind, .. } => Some(*kind), + } + } + + /// Borrow the exact execution target reference, if one was resolved. + pub fn execution_ref(&self) -> Option<&ResourceRef> { + match self { + Self::Zone(_) => None, + Self::Execution { reference, .. } => Some(reference), + } + } +} + /// Extension-schema layer encoded in a schema ID. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ExtensionSchemaLayer { @@ -1185,6 +1313,10 @@ pub enum ResourceSchemaError { ProviderSchemaMismatch, ProviderFieldShadowsBase(String), ProviderExtensionNotMinimal, + PlacementAnchorMismatch, + PlacementTargetMissing, + PlacementTargetInvalid, + PlacementTargetWrongType, } impl core::fmt::Debug for ResourceSchemaError { @@ -1207,6 +1339,10 @@ impl core::fmt::Debug for ResourceSchemaError { Self::ProviderSchemaMismatch => "ProviderSchemaMismatch", Self::ProviderFieldShadowsBase(_) => "ProviderFieldShadowsBase", Self::ProviderExtensionNotMinimal => "ProviderExtensionNotMinimal", + Self::PlacementAnchorMismatch => "PlacementAnchorMismatch", + Self::PlacementTargetMissing => "PlacementTargetMissing", + Self::PlacementTargetInvalid => "PlacementTargetInvalid", + Self::PlacementTargetWrongType => "PlacementTargetWrongType", }; write!(f, "ResourceSchemaError::{kind}") } @@ -1254,6 +1390,18 @@ impl core::fmt::Display for ResourceSchemaError { Self::ProviderExtensionNotMinimal => { f.write_str("minimal base spec must not require a Provider extension") } + Self::PlacementAnchorMismatch => { + f.write_str("placement anchor does not match the registered ResourceType schema") + } + Self::PlacementTargetMissing => { + f.write_str("placement anchor field is missing from the committed resource") + } + Self::PlacementTargetInvalid => { + f.write_str("placement anchor value is not a valid target reference") + } + Self::PlacementTargetWrongType => { + f.write_str("placement anchor must resolve to a Host or Guest") + } } } } @@ -1514,6 +1662,34 @@ key":1}"#, ); } + #[test] + fn placement_anchor_registry_is_closed_and_resolves_execution_targets() { + let process = ResourceTypeName::parse("Process").unwrap(); + let generation = + ResourceTypeName::parse("activation-nixos.d2bus.org.NixosGeneration").unwrap(); + let shell = ResourceTypeName::parse("shell-terminal.d2bus.org.ShellSession").unwrap(); + let wayland = + ResourceTypeName::parse("display-wayland.d2bus.org.WaylandSession").unwrap(); + + assert_eq!( + PlacementAnchor::canonical_for(&process), + Some(PlacementAnchor::ExecutionRef) + ); + assert_eq!( + PlacementAnchor::canonical_for(&generation), + Some(PlacementAnchor::ExecutionRef) + ); + assert_eq!( + PlacementAnchor::canonical_for(&shell), + Some(PlacementAnchor::ExecutionRef) + ); + assert_eq!( + PlacementAnchor::canonical_for(&wayland), + Some(PlacementAnchor::Zone) + ); + assert!(serde_json::from_str::("\"field-path\"").is_err()); + } + #[test] fn minimal_base_and_base_binding_conformance_are_exact() { let contract = contract(); diff --git a/packages/d2b-contracts-resource/src/v3/volume.rs b/packages/d2b-contracts-resource/src/v3/volume.rs index 89dc4d4cf2..196a5abe9b 100644 --- a/packages/d2b-contracts-resource/src/v3/volume.rs +++ b/packages/d2b-contracts-resource/src/v3/volume.rs @@ -292,7 +292,6 @@ pub enum CleanupPolicy { Boot, ProcessExitWithProof, VmStopWithProof, - CutoverOnly, External, /// Compatibility spelling for controller-owned cleanup. OwnerControlled, diff --git a/packages/d2b-contracts-zone-session/src/v3/component_session.rs b/packages/d2b-contracts-zone-session/src/v3/component_session.rs index 8676cbd95c..77198fb355 100644 --- a/packages/d2b-contracts-zone-session/src/v3/component_session.rs +++ b/packages/d2b-contracts-zone-session/src/v3/component_session.rs @@ -308,7 +308,7 @@ closed_enum!(EndpointPurpose { ResourceService = 2 => "resource-service", ZoneLink = 3 => "zone-link", Bootstrap = 4 => "bootstrap", - GuestControl = 5 => "guest-control", + ComponentSession = 5 => "component-session", ResourceTransfer = 6 => "resource-transfer", ProviderControl = 7 => "provider-control", SensitiveCredential = 8 => "sensitive-credential", @@ -347,7 +347,8 @@ closed_enum!(ServicePackage { ClipboardV3 = 10 => "d2b.clipboard.v3", ClipboardBridgeV3 = 11 => "d2b.clipboard.bridge.v3", ClipboardPickerCoordV3 = 12 => "d2b.clipboard.picker-coord.v3", - NotificationV3 = 13 => "d2b.notification.v3" + NotificationV3 = 13 => "d2b.notification.v3", + ConfigNixosV3 = 14 => "d2b.config-nixos.v3" }); closed_enum!(NoiseProfile { @@ -782,6 +783,37 @@ impl EndpointPolicyIdentity { Ok(()) } + /// Validate an identity permitted to request reconnect generation + /// discovery. Discovery does not authenticate static keys; the subsequent + /// handshake remains responsible for exact policy and key authentication. + pub fn validate_generation_discovery(&self) -> Result<(), ContractError> { + self.validate()?; + let local = self.purpose_class == PurposeClass::Local + && self.noise_profile == NoiseProfile::Nn25519ChaChaPolySha256 + && self.transport_binding.identity_evidence + == IdentityEvidenceRequirement::DirectionalUnix + && matches!( + self.transport_binding.transport, + TransportClass::UnixStream | TransportClass::UnixSeqpacket + ); + let enrolled_guest = self.purpose == EndpointPurpose::ZoneLink + && self.purpose_class == PurposeClass::Enrolled + && self.initiator_role == EndpointRole::ZoneController + && self.responder_role == EndpointRole::GuestAgent + && self.service == ServicePackage::ResourceV3 + && self.noise_profile == NoiseProfile::Kk25519ChaChaPolySha256 + && self.transport_binding.transport == TransportClass::NativeVsock + && self.transport_binding.locality == Locality::GuestLocal + && self.transport_binding.identity_evidence + == IdentityEvidenceRequirement::EnrolledStaticKeys + && self.attachment_policy == AttachmentPolicy::disabled(); + if local || enrolled_guest { + Ok(()) + } else { + Err(ContractError::IdentityEvidenceMismatch) + } + } + pub fn with_generation( &self, reconnect_generation: u64, diff --git a/packages/d2b-contracts-zone-session/src/v3/services.rs b/packages/d2b-contracts-zone-session/src/v3/services.rs index ea3073684a..e7127cfb6a 100644 --- a/packages/d2b-contracts-zone-session/src/v3/services.rs +++ b/packages/d2b-contracts-zone-session/src/v3/services.rs @@ -33,6 +33,7 @@ pub enum V3Service { Audit, Support, Credential, + ConfigNixos, } impl V3Service { @@ -47,6 +48,7 @@ impl V3Service { Self::Audit => "d2b.audit.v3", Self::Support => "d2b.support.v3", Self::Credential => "d2b.credential.v3", + Self::ConfigNixos => "d2b.config-nixos.v3", } } @@ -58,7 +60,7 @@ impl V3Service { } /// Every service package in stable order. - pub const ALL: [Self; 8] = [ + pub const ALL: [Self; 9] = [ Self::Resource, Self::Zone, Self::ZoneLink, @@ -67,6 +69,7 @@ impl V3Service { Self::Audit, Self::Support, Self::Credential, + Self::ConfigNixos, ]; } diff --git a/packages/d2b-contracts-zone-session/src/v3/zone_session.rs b/packages/d2b-contracts-zone-session/src/v3/zone_session.rs index a989ec1e25..f94c063bf6 100644 --- a/packages/d2b-contracts-zone-session/src/v3/zone_session.rs +++ b/packages/d2b-contracts-zone-session/src/v3/zone_session.rs @@ -173,8 +173,8 @@ zone_closed_enum!( ZoneLink = 3 => "zone-link", /// The one-time IKpsk2 enrollment bootstrap, which terminates. Bootstrap = 4 => "bootstrap", - /// Guest control traffic. - GuestControl = 5 => "guest-control", + /// ComponentSession traffic. + ComponentSession = 5 => "component-session", /// Bulk resource transfer. ResourceTransfer = 6 => "resource-transfer", /// Provider agent control traffic. @@ -232,7 +232,8 @@ zone_closed_enum!( /// Tags 1 through 6 are preserved from the component-session assignment. /// Tags 7 and 8 are the appended Zone service packages. Tags 9 through /// 13 carry the interaction Provider packages shared with - /// ComponentSession. Protobuf field numbers for the v3 services are + /// ComponentSession. Tag 14 carries the service-only config-nixos + /// Provider. Protobuf field numbers for the v3 services are /// frozen independently of the v2 assignments and are not restated by /// this module. ServicePackage { @@ -262,6 +263,8 @@ zone_closed_enum!( ClipboardPickerCoordV3 = 12 => "d2b.clipboard.picker-coord.v3", /// The desktop notification Provider package. NotificationV3 = 13 => "d2b.notification.v3", + /// The service-only NixOS configuration Provider package. + ConfigNixosV3 = 14 => "d2b.config-nixos.v3", } ); @@ -345,7 +348,7 @@ mod tests { (EndpointPurpose::ResourceService, 2, "resource-service"), (EndpointPurpose::ZoneLink, 3, "zone-link"), (EndpointPurpose::Bootstrap, 4, "bootstrap"), - (EndpointPurpose::GuestControl, 5, "guest-control"), + (EndpointPurpose::ComponentSession, 5, "component-session"), (EndpointPurpose::ResourceTransfer, 6, "resource-transfer"), (EndpointPurpose::ProviderControl, 7, "provider-control"), ( @@ -395,6 +398,11 @@ mod tests { "d2b.clipboard.picker-coord.v3", ), (ServicePackage::NotificationV3, 13, "d2b.notification.v3"), + ( + ServicePackage::ConfigNixosV3, + 14, + "d2b.config-nixos.v3", + ), ]; #[test] @@ -493,7 +501,7 @@ mod tests { Err(BinaryError::UnknownEnumTag) ); assert_eq!( - ServicePackage::from_tag(14), + ServicePackage::from_tag(15), Err(BinaryError::UnknownEnumTag) ); } diff --git a/packages/d2b-contracts-zone-session/tests/contracts.rs b/packages/d2b-contracts-zone-session/tests/contracts.rs index df1a97b94e..9527a4ed27 100644 --- a/packages/d2b-contracts-zone-session/tests/contracts.rs +++ b/packages/d2b-contracts-zone-session/tests/contracts.rs @@ -1,5 +1,12 @@ -use d2b_contracts_zone_session::v3::ZoneSpec; use d2b_contracts_resource::v3::ZoneId; +use d2b_contracts_zone_session::v3::{ + ZoneSpec, + component_session::{ + AttachmentPolicy, AttachmentPolicyKind, EndpointPolicyIdentity, EndpointPurpose, + EndpointRole, IdentityEvidenceRequirement, LimitProfile, Locality, NoiseProfile, + PurposeClass, ServicePackage, TransportBinding, TransportClass, + }, +}; #[test] fn zone_session_contracts_preserve_zone_identity() { @@ -7,3 +14,73 @@ fn zone_session_contracts_preserve_zone_identity() { let _ = std::any::type_name::(); assert_eq!(zone.as_str(), "work"); } + +fn enrolled_guest_discovery_identity() -> EndpointPolicyIdentity { + EndpointPolicyIdentity { + purpose: EndpointPurpose::ZoneLink, + purpose_class: PurposeClass::Enrolled, + initiator_role: EndpointRole::ZoneController, + responder_role: EndpointRole::GuestAgent, + service: ServicePackage::ResourceV3, + schema_fingerprint: [0x11; 32], + noise_profile: NoiseProfile::Kk25519ChaChaPolySha256, + limits: LimitProfile::remote_default(), + transport_binding: TransportBinding { + transport: TransportClass::NativeVsock, + locality: Locality::GuestLocal, + channel_binding: [0x22; 32], + identity_evidence: IdentityEvidenceRequirement::EnrolledStaticKeys, + }, + attachment_policy: AttachmentPolicy::disabled(), + } +} + +#[test] +fn generation_discovery_accepts_only_the_exact_enrolled_guest_profile() { + let identity = enrolled_guest_discovery_identity(); + assert!(identity.validate_generation_discovery().is_ok()); + + let mut wrong_role = identity.clone(); + wrong_role.responder_role = EndpointRole::Relay; + assert!(wrong_role.validate_generation_discovery().is_err()); + + let mut wrong_purpose = identity.clone(); + wrong_purpose.purpose = EndpointPurpose::ResourceService; + assert!(wrong_purpose.validate_generation_discovery().is_err()); + + let mut wrong_noise = identity.clone(); + wrong_noise.noise_profile = NoiseProfile::Nn25519ChaChaPolySha256; + assert!(wrong_noise.validate_generation_discovery().is_err()); + + let mut wrong_evidence = identity.clone(); + wrong_evidence.transport_binding.identity_evidence = + IdentityEvidenceRequirement::DirectionalUnix; + assert!(wrong_evidence.validate_generation_discovery().is_err()); + + let mut wrong_transport = identity.clone(); + wrong_transport.transport_binding.transport = TransportClass::ProviderStream; + assert!(wrong_transport.validate_generation_discovery().is_err()); + + let mut wrong_locality = identity.clone(); + wrong_locality.transport_binding.locality = Locality::Remote; + assert!(wrong_locality.validate_generation_discovery().is_err()); + + let mut attachments = identity.clone(); + attachments.attachment_policy = AttachmentPolicy { + kind: AttachmentPolicyKind::PacketAtomic, + max_per_packet: 1, + max_per_request: 1, + max_per_operation: 1, + max_per_session: 1, + credentials_allowed: false, + }; + assert!(attachments.validate_generation_discovery().is_err()); + + let mut zero_schema = identity.clone(); + zero_schema.schema_fingerprint = [0; 32]; + assert!(zero_schema.validate_generation_discovery().is_err()); + + let mut zero_channel_binding = identity; + zero_channel_binding.transport_binding.channel_binding = [0; 32]; + assert!(zero_channel_binding.validate_generation_discovery().is_err()); +} diff --git a/packages/d2b-contracts/src/error.rs b/packages/d2b-contracts/src/error.rs index 85eb211de9..83ce7a99b6 100644 --- a/packages/d2b-contracts/src/error.rs +++ b/packages/d2b-contracts/src/error.rs @@ -50,8 +50,8 @@ pub enum Kind { #[serde(rename = "bundle-tampered")] BundleTampered, /// A provider required by an audio or console operation is present but - /// not in a state where enforcement can proceed (e.g. expected guestd - /// agent absent). Operator remediation required. + /// not in a state where enforcement can proceed (e.g. expected + /// target-local Process absent). Operator remediation required. #[serde(rename = "provider-misconfigured")] ProviderMisconfigured, #[serde(rename = "provider-artifact-output-ambiguous")] @@ -228,7 +228,7 @@ static ERROR_KIND_RECORDS: [ErrorKindRecord; 37] = [ exit_code: 80, owning_command: "provider", message_template: "provider for {vm} is misconfigured: {reason}", - remediation: "Check the provider configuration for the VM and verify the expected guestd-compatible agent or sidecar is running.", + remediation: "Check the provider configuration for the VM and verify the expected target-local Process or sidecar is running.", docs_anchor: "#provider-misconfigured", }, ErrorKindRecord { @@ -630,7 +630,7 @@ impl Error { } /// Provider required by an audio or console operation is present but - /// misconfigured (e.g. expected guestd agent absent). + /// misconfigured (e.g. expected target-local Process absent). pub fn provider_misconfigured(vm: impl Into, reason: impl Into) -> Self { Self::Audio(AudioError::ProviderMisconfigured { vm: vm.into(), @@ -755,7 +755,7 @@ impl From for Error { #[derive(Debug, Clone, PartialEq, Eq)] pub enum AudioError { /// Provider required by an audio or console operation is present but - /// misconfigured (e.g. expected guestd agent absent). + /// misconfigured (e.g. expected target-local Process absent). ProviderMisconfigured { vm: String, reason: String }, } @@ -1421,7 +1421,7 @@ mod tests { "internal-io", ), ( - Error::provider_misconfigured("corp-vm", "guestd agent not found"), + Error::provider_misconfigured("corp-vm", "target-local Process not found"), "provider-misconfigured", ), // BundleTampered: the message contains the path, so the diff --git a/packages/d2b-contracts/src/lib.rs b/packages/d2b-contracts/src/lib.rs index 350248fbb7..588380e371 100644 --- a/packages/d2b-contracts/src/lib.rs +++ b/packages/d2b-contracts/src/lib.rs @@ -74,7 +74,6 @@ impl FeatureFlag { "export-broker-audit" => Some(KnownFeatureFlag::ExportBrokerAudit), "configured-launch-v1" => Some(KnownFeatureFlag::ConfiguredLaunchV1), "unsafe-local-provider-v1" => Some(KnownFeatureFlag::UnsafeLocalProviderV1), - "cutover-runner-v1" => Some(KnownFeatureFlag::CutoverRunnerV1), _ => None, } } @@ -108,7 +107,6 @@ pub enum KnownFeatureFlag { ExportBrokerAudit, ConfiguredLaunchV1, UnsafeLocalProviderV1, - CutoverRunnerV1, } impl KnownFeatureFlag { @@ -120,7 +118,6 @@ impl KnownFeatureFlag { Self::ExportBrokerAudit => "export-broker-audit", Self::ConfiguredLaunchV1 => "configured-launch-v1", Self::UnsafeLocalProviderV1 => "unsafe-local-provider-v1", - Self::CutoverRunnerV1 => "cutover-runner-v1", } } diff --git a/packages/d2b-contracts/src/privileges_w3.rs b/packages/d2b-contracts/src/privileges_w3.rs index ec065c93b0..b5981d22bc 100644 --- a/packages/d2b-contracts/src/privileges_w3.rs +++ b/packages/d2b-contracts/src/privileges_w3.rs @@ -46,13 +46,9 @@ pub enum W3BrokerOperation { UpdateHostsFile, BindUnixSocket, SetSocketAcl, - GuestControlSign, ModprobeIfAllowed, UsbipBindFirewallRule, MigrateLegacySwtpmState, - LaunchCutoverRunner, - CutoverAudit, - CutoverEffect, /// Open the FIDO/CTAP hidraw node for the broker-configured device /// selector. Typed stub until the live host-broker handler is implemented. SecurityKeyOpenDevice, @@ -89,13 +85,9 @@ impl W3BrokerOperation { Self::UpdateHostsFile => "UpdateHostsFile", Self::BindUnixSocket => "BindUnixSocket", Self::SetSocketAcl => "SetSocketAcl", - Self::GuestControlSign => "GuestControlSign", Self::ModprobeIfAllowed => "ModprobeIfAllowed", Self::UsbipBindFirewallRule => "UsbipBindFirewallRule", Self::MigrateLegacySwtpmState => "MigrateLegacySwtpmState", - Self::LaunchCutoverRunner => "LaunchCutoverRunner", - Self::CutoverAudit => "CutoverAudit", - Self::CutoverEffect => "CutoverEffect", Self::SecurityKeyOpenDevice => "SecurityKeyOpenDevice", Self::SecurityKeyApplyUdevRules => "SecurityKeyApplyUdevRules", } @@ -128,13 +120,9 @@ impl W3BrokerOperation { Self::UpdateHostsFile, Self::BindUnixSocket, Self::SetSocketAcl, - Self::GuestControlSign, Self::ModprobeIfAllowed, Self::UsbipBindFirewallRule, Self::MigrateLegacySwtpmState, - Self::LaunchCutoverRunner, - Self::CutoverAudit, - Self::CutoverEffect, Self::SecurityKeyOpenDevice, Self::SecurityKeyApplyUdevRules, ] @@ -186,11 +174,6 @@ impl W3BrokerOperation { destructive: true, secret_access: false, }, - Self::GuestControlSign => W3OperationFlags { - audit: true, - destructive: false, - secret_access: true, - }, Self::ModprobeIfAllowed => W3OperationFlags { audit: true, destructive: true, @@ -206,16 +189,6 @@ impl W3BrokerOperation { destructive: true, secret_access: false, }, - Self::LaunchCutoverRunner | Self::CutoverEffect => W3OperationFlags { - audit: true, - destructive: true, - secret_access: false, - }, - Self::CutoverAudit => W3OperationFlags { - audit: true, - destructive: false, - secret_access: false, - }, // SecurityKeyOpenDevice: opens a single FIDO hidraw fd; read-only // from the broker's perspective (no state mutation, no secret data). Self::SecurityKeyOpenDevice => W3OperationFlags { @@ -270,17 +243,14 @@ mod tests { assert!(W3BrokerOperation::DeleteBridge.flags().destructive); assert!(W3BrokerOperation::DeletePersistentTap.flags().destructive); assert!(!W3BrokerOperation::UsbipBindFirewallRule.flags().destructive); - assert!(W3BrokerOperation::LaunchCutoverRunner.flags().destructive); - assert!(!W3BrokerOperation::CutoverAudit.flags().destructive); - assert!(W3BrokerOperation::CutoverEffect.flags().destructive); } #[test] - fn only_guest_control_sign_grants_secret_access() { + fn no_remaining_operation_grants_secret_access() { for op in W3BrokerOperation::all() { assert_eq!( op.flags().secret_access, - *op == W3BrokerOperation::GuestControlSign, + false, "unexpected secret_access flag for {op:?}" ); } diff --git a/packages/d2b-contracts/src/runtime.rs b/packages/d2b-contracts/src/runtime.rs index 6a07253046..a403e620df 100644 --- a/packages/d2b-contracts/src/runtime.rs +++ b/packages/d2b-contracts/src/runtime.rs @@ -76,7 +76,6 @@ pub struct RuntimeDisplayCapabilities { pub struct RuntimeGuestCapabilities { pub config_sync: bool, pub exec: bool, - pub guest_control: bool, pub in_guest_observability: bool, pub keys: bool, #[serde(default)] @@ -105,7 +104,7 @@ pub enum RuntimeServiceRole { Audio, Video, Network, - GuestControl, + ComponentSession, Usb, Observability, } diff --git a/packages/d2b-contracts/src/usbip.rs b/packages/d2b-contracts/src/usbip.rs index d76df708ab..423f511d03 100644 --- a/packages/d2b-contracts/src/usbip.rs +++ b/packages/d2b-contracts/src/usbip.rs @@ -1,6 +1,6 @@ //! Shared USBIP wire validation helpers and explicit-attach DTOs. //! -//! Bus IDs cross the CLI, daemon, broker, and guest-control boundary. Keep the +//! Bus IDs cross the CLI, daemon, broker, and component-session boundary. Keep the //! shape check here so every layer rejects the same traversal-/shell-unsafe //! strings before they reach a subprocess argv. //! diff --git a/packages/d2b-controller-toolkit/benches/support/reaction.rs b/packages/d2b-controller-toolkit/benches/support/reaction.rs index bb1eb9b3a5..f86e0dbbf1 100644 --- a/packages/d2b-controller-toolkit/benches/support/reaction.rs +++ b/packages/d2b-controller-toolkit/benches/support/reaction.rs @@ -119,6 +119,7 @@ impl ProductionStore { remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(payload_digest), @@ -160,6 +161,7 @@ impl ProductionStore { remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, Some(resource.uid.clone()), Some(digest), diff --git a/packages/d2b-controller-toolkit/tests/support/mod.rs b/packages/d2b-controller-toolkit/tests/support/mod.rs index 6f1bd4397c..a2da3c2d0e 100644 --- a/packages/d2b-controller-toolkit/tests/support/mod.rs +++ b/packages/d2b-controller-toolkit/tests/support/mod.rs @@ -129,6 +129,7 @@ pub async fn commit_host( remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(digest), diff --git a/packages/d2b-core-controller/src/api_catalog.rs b/packages/d2b-core-controller/src/api_catalog.rs index 95608be0e0..cef55b8c89 100644 --- a/packages/d2b-core-controller/src/api_catalog.rs +++ b/packages/d2b-core-controller/src/api_catalog.rs @@ -2,15 +2,8 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_provider::v3::{ - ProviderManifest, - ResourceApiBinding, -}; -use d2b_contracts_resource::v3::{ - ResourceRef, - ResourceTypeName, - SchemaFingerprint, -}; +use d2b_contracts_provider::v3::{ProviderManifest, ResourceApiBinding}; +use d2b_contracts_resource::v3::{ResourceRef, ResourceTypeName, SchemaFingerprint}; /// Closed reason a catalog candidate was refused. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -252,27 +245,18 @@ impl ApiCatalogHandler { #[cfg(test)] mod tests { + use d2b_contracts_provider::v3::UpgradePolicy as ProviderUpgradePolicy; use d2b_contracts_provider::v3::{ - UpgradePolicy as ProviderUpgradePolicy, -}; - use d2b_contracts_provider::v3::{ - ArtifactDigest, - ArtifactDigestSet, - CompatibilityRange, - ComponentDescriptor, - ComponentType, - PolicyEvaluation, - RevocationState, - SignatureState, - StandardCapabilityMatrix, - TrustEvidence, - UpgradeDisposition, -}; -use d2b_contracts_resource::v3::{ - ArtifactId, - SchemaVersion, - execution_policy::{BoundedToken, ExecutionDomain}, -}; + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, RevocationState, SignatureState, + StandardCapabilityMatrix, TargetRuntimeArtifacts, TrustEvidence, UpgradeDisposition, + }; + use d2b_contracts_resource::v3::{ + ArtifactId, SchemaVersion, + execution_policy::{BoundedToken, ExecutionDomain}, + resource_schema::PlacementAnchor, + }; use super::*; @@ -326,8 +310,31 @@ use d2b_contracts_resource::v3::{ [], false, ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) .unwrap()], - [ResourceApiBinding::new( + [ResourceApiBinding::new_with_placement( resource_type, SchemaVersion::new(1, 0).unwrap(), fingerprint('2'), @@ -336,6 +343,7 @@ use d2b_contracts_resource::v3::{ StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::ExecutionRef, ) .unwrap()], [], @@ -346,6 +354,11 @@ use d2b_contracts_resource::v3::{ }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new(ControllerTargetKind::Host, digest(), digest()).unwrap(), + TargetRuntimeArtifacts::new(ControllerTargetKind::Guest, digest(), digest()).unwrap(), + ]) + .unwrap() } fn admission<'a>( diff --git a/packages/d2b-core-controller/src/audit.rs b/packages/d2b-core-controller/src/audit.rs index 5eb0f033f6..c8029b6f6b 100644 --- a/packages/d2b-core-controller/src/audit.rs +++ b/packages/d2b-core-controller/src/audit.rs @@ -9,15 +9,8 @@ use std::collections::BTreeSet; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - ResourceName, - ResourceTypeName, - SchemaFingerprint, - Timestamp, - ZoneId, - ZoneRevision, - canonical_digest, + ConfigurationGeneration, ResourceBundleGenerationId, ResourceName, ResourceTypeName, + SchemaFingerprint, Timestamp, ZoneId, ZoneRevision, canonical_digest, }; /// Domain tag for the digest of a resource name in an audit record. diff --git a/packages/d2b-core-controller/src/authority.rs b/packages/d2b-core-controller/src/authority.rs index 070c543f6b..81ecb12b9d 100644 --- a/packages/d2b-core-controller/src/authority.rs +++ b/packages/d2b-core-controller/src/authority.rs @@ -15,14 +15,12 @@ use std::{ }; use d2b_contracts_resource::v3::{ - CanonicalJsonValue, - IfName, - ResourceGeneration, - ResourceRef, - ResourceUid, - UpdateState, + CanonicalJsonValue, IfName, ResourceGeneration, ResourceRef, ResourceUid, UpdateState, is_canonical_digest, - network::{ExternalNicAdmissionError, ExternalNicAuthorityStatus, ExternalNicClaim, MacvtapMode, SharingPolicy, admit_external_nic_claims}, + network::{ + ExternalNicAdmissionError, ExternalNicAuthorityStatus, ExternalNicClaim, MacvtapMode, + SharingPolicy, admit_external_nic_claims, + }, process::PortProtocol, resource_schema::canonical_digest, }; diff --git a/packages/d2b-core-controller/src/authz.rs b/packages/d2b-core-controller/src/authz.rs index d4b664f36f..15f491fcc4 100644 --- a/packages/d2b-core-controller/src/authz.rs +++ b/packages/d2b-core-controller/src/authz.rs @@ -2,21 +2,12 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_resource::v3::{ - ControllerGeneration, - ResourceGeneration, - ResourceName, - ResourceRef, - ResourceTypeName, - ResourceUid, - SchemaFingerprint, -}; use d2b_contracts_resource::v3::identity::{ - AuthenticatedSubjectContext, - EvidenceClass, - Locality, - ServiceName, - SessionPurpose, + AuthenticatedSubjectContext, EvidenceClass, Locality, ServiceName, SessionPurpose, +}; +use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceName, ResourceRef, ResourceTypeName, + ResourceUid, SchemaFingerprint, }; /// Borrowed subject evidence admitted by the authenticated session adapter. @@ -319,15 +310,10 @@ impl AuthorizationHandler { #[cfg(test)] mod tests { use d2b_contracts_resource::v3::SchemaFingerprint; -use d2b_contracts_resource::v3::identity::{ - BindingDigest, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, -}; + use d2b_contracts_resource::v3::identity::{ + BindingDigest, ReconnectGeneration, ServiceName, SessionBinding, SessionPurpose, + TranscriptHash, TransportBinding, + }; use super::*; diff --git a/packages/d2b-core-controller/src/binding_children.rs b/packages/d2b-core-controller/src/binding_children.rs new file mode 100644 index 0000000000..4ce12f3d33 --- /dev/null +++ b/packages/d2b-core-controller/src/binding_children.rs @@ -0,0 +1,950 @@ +//! Core-owned validation and reconciliation of semantic Binding children. +//! +//! Providers describe child intent and supply the signed desired resource +//! bodies. Core is the only layer that turns those declarations into owner +//! repair plans, preserving UID/revision preconditions and the authoritative +//! owner index across restart and disconnect. + +use std::collections::{BTreeMap, BTreeSet}; + +use d2b_contracts_provider::v3::semantic_services::child_resources::{ + BindingChildIntent, BindingChildKind, BindingChildPlacement, BindingChildSet, +}; +use d2b_contracts_resource::v3::{ + CanonicalJsonValue, RESOURCE_ENVELOPE_DOMAIN_TAG, ResourceRef, ResourceTypeName, ZoneRevision, + canonical_digest, +}; +use d2b_contracts_zone_session::v3::resource_bundle::BundleResource; + +use crate::{ + DesiredChild, HintTarget, ObservedChild, OwnerIndex, OwnerLimits, OwnerReconcileError, + OwnerReconcilePlan, +}; + +/// One provider-supplied desired child body paired with its semantic intent. +#[derive(Clone, PartialEq, Eq)] +pub struct BindingChildResource { + intent: BindingChildIntent, + canonical_resource: Vec, +} + +impl core::fmt::Debug for BindingChildResource { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("BindingChildResource") + .field("resource_type", &self.intent.resource_ref().resource_type()) + .field("has_canonical_resource", &true) + .finish() + } +} + +impl BindingChildResource { + /// Validate and pair one signed desired child body with its intent. + pub fn new( + intent: BindingChildIntent, + canonical_resource: Vec, + ) -> Result { + if canonical_resource.is_empty() { + return Err(BindingChildMaterializationError::EmptyResource); + } + let resource: BundleResource = serde_json::from_slice(&canonical_resource) + .map_err(|_| BindingChildMaterializationError::MalformedResource)?; + let expected_type = ResourceTypeName::parse(intent.kind().resource_type()) + .expect("closed child ResourceType is valid"); + let actual_ref = ResourceRef::new( + resource.resource_type().clone(), + resource.metadata().name().clone(), + ); + if resource.resource_type() != &expected_type || &actual_ref != intent.resource_ref() { + return Err(BindingChildMaterializationError::IdentityMismatch); + } + if resource.metadata().owner_ref() != Some(intent.owner_ref()) { + return Err(BindingChildMaterializationError::OwnerMismatch); + } + validate_spec(&intent, resource.spec())?; + let canonical = CanonicalJsonValue::parse(&canonical_resource) + .map_err(|_| BindingChildMaterializationError::NonCanonicalResource)? + .to_canonical_bytes(); + if canonical != canonical_resource { + return Err(BindingChildMaterializationError::NonCanonicalResource); + } + Ok(Self { + intent, + canonical_resource, + }) + } + + /// Borrow the validated child intent. + pub const fn intent(&self) -> &BindingChildIntent { + &self.intent + } + + /// Borrow the canonical desired body. + pub fn canonical_resource(&self) -> &[u8] { + &self.canonical_resource + } + + /// Materialize the complete create payload Core submits to the Resource + /// API for this child. + /// + /// Providers only declare UID-free intent. Core supplies the common + /// metadata and status envelope while the store remains responsible for + /// minting the authoritative UID and revision. + pub fn create_payload( + &self, + zone: &d2b_contracts_resource::v3::ZoneId, + ) -> Result, BindingChildMaterializationError> { + let resource: BundleResource = serde_json::from_slice(&self.canonical_resource) + .map_err(|_| BindingChildMaterializationError::MalformedResource)?; + if resource.metadata().zone() != zone { + return Err(BindingChildMaterializationError::OwnerMismatch); + } + materialize_child_create_payload(&self.intent, zone) + } +} + +/// Compute the stable desired-state digest for a child resource. +/// +/// Resource API payloads gain UID, revision, timestamps, status, and +/// controller metadata when they enter the store. Those fields are runtime +/// state, not Provider intent, so owner repair compares only the identity, +/// ownership, presentation metadata, and spec layers. +pub fn semantic_child_digest( + canonical_resource: &[u8], +) -> Result { + let value = CanonicalJsonValue::parse(canonical_resource) + .map_err(|_| BindingChildMaterializationError::MalformedResource)?; + let CanonicalJsonValue::Object(root) = value else { + return Err(BindingChildMaterializationError::MalformedResource); + }; + let Some(resource_type) = root.get("type") else { + return Err(BindingChildMaterializationError::MalformedResource); + }; + let Some(CanonicalJsonValue::Object(metadata)) = root.get("metadata") else { + return Err(BindingChildMaterializationError::MalformedResource); + }; + let Some(spec) = root.get("spec") else { + return Err(BindingChildMaterializationError::MalformedResource); + }; + let Some(name) = metadata.get("name") else { + return Err(BindingChildMaterializationError::MalformedResource); + }; + let Some(zone) = metadata.get("zone") else { + return Err(BindingChildMaterializationError::MalformedResource); + }; + let mut semantic_metadata = BTreeMap::new(); + semantic_metadata.insert("name".to_owned(), name.clone()); + semantic_metadata.insert("zone".to_owned(), zone.clone()); + semantic_metadata.insert( + "ownerRef".to_owned(), + metadata + .get("ownerRef") + .cloned() + .unwrap_or(CanonicalJsonValue::Null), + ); + semantic_metadata.insert( + "labels".to_owned(), + metadata + .get("labels") + .cloned() + .unwrap_or_else(|| CanonicalJsonValue::Object(BTreeMap::new())), + ); + semantic_metadata.insert( + "annotations".to_owned(), + metadata + .get("annotations") + .cloned() + .unwrap_or_else(|| CanonicalJsonValue::Object(BTreeMap::new())), + ); + let mut semantic = BTreeMap::new(); + semantic.insert("type".to_owned(), resource_type.clone()); + semantic.insert( + "metadata".to_owned(), + CanonicalJsonValue::Object(semantic_metadata), + ); + semantic.insert("spec".to_owned(), spec.clone()); + let canonical = CanonicalJsonValue::Object(semantic).to_canonical_bytes(); + Ok(canonical_digest(RESOURCE_ENVELOPE_DOMAIN_TAG, &canonical)) +} + +/// Build an observed child row from a complete Resource API envelope. +/// +/// This is the Core-side adapter used after a relist. It deliberately derives +/// the digest from the stored body instead of trusting a Provider-supplied +/// payload digest, keeping UID/revision fencing separate from desired-state +/// convergence. +pub fn observed_child_from_resource( + target: HintTarget, + revision: ZoneRevision, + canonical_resource: &[u8], + deletion_requested: bool, + deletion_ready: bool, +) -> Result { + let digest = semantic_child_digest(canonical_resource)?; + ObservedChild::with_deletion_state(target, revision, digest, deletion_requested, deletion_ready) + .map_err(BindingChildMaterializationError::OwnerReconcile) +} + +/// Build the canonical, UID-free Resource API create payload for one child. +/// +/// This is intentionally owned by Core: Providers cannot smuggle arbitrary +/// resource envelopes or choose a different Process Provider. The semantic +/// Provider remains the Endpoint owner, while Process execution is delegated +/// to the fixed system Process Provider. +pub fn materialize_child_create_payload( + intent: &BindingChildIntent, + zone: &d2b_contracts_resource::v3::ZoneId, +) -> Result, BindingChildMaterializationError> { + let owner_ref = intent.owner_ref().to_canonical_string(); + let provider_ref = match intent.kind() { + BindingChildKind::Process | BindingChildKind::EphemeralProcess => { + "Provider/system-systemd".to_owned() + } + BindingChildKind::Endpoint => intent.provider_ref().to_canonical_string(), + }; + let mut spec = serde_json::Map::new(); + match intent.kind() { + BindingChildKind::Process | BindingChildKind::EphemeralProcess => { + let process_provider = intent + .process_provider() + .unwrap_or("Provider/system-systemd"); + let process_template = intent.process_template().unwrap_or_else(|| intent.role()); + let process_domain = intent.process_domain().map(|domain| match domain { + d2b_contracts_resource::v3::ExecutionDomain::System => "system", + d2b_contracts_resource::v3::ExecutionDomain::User => "user", + }); + let process_class = + intent + .process_class() + .unwrap_or(if intent.kind() == BindingChildKind::Process { + "service" + } else { + "worker" + }); + spec.insert( + "executionRef".to_owned(), + serde_json::Value::String(intent.execution_ref().to_canonical_string()), + ); + spec.insert( + "processClass".to_owned(), + serde_json::Value::String(process_class.to_owned()), + ); + spec.insert( + "template".to_owned(), + serde_json::Value::String(process_template.to_owned()), + ); + spec.insert( + "providerRef".to_owned(), + serde_json::Value::String(process_provider.to_owned()), + ); + if let Some(process_domain) = process_domain { + spec.insert( + "domain".to_owned(), + serde_json::Value::String(process_domain.to_owned()), + ); + } + if let Some(user_ref) = intent.process_user() { + spec.insert( + "userRef".to_owned(), + serde_json::Value::String(user_ref.to_canonical_string()), + ); + } + } + BindingChildKind::Endpoint => { + let producer = intent + .producer_ref() + .ok_or(BindingChildMaterializationError::ProducerMismatch)?; + spec.insert( + "providerRef".to_owned(), + serde_json::Value::String(provider_ref), + ); + spec.insert( + "producerRef".to_owned(), + serde_json::Value::String(producer.to_canonical_string()), + ); + spec.insert( + "endpointClass".to_owned(), + serde_json::Value::String("service".to_owned()), + ); + spec.insert( + "transport".to_owned(), + serde_json::Value::String("opaque-carriage".to_owned()), + ); + spec.insert( + "purpose".to_owned(), + serde_json::Value::String(intent.role().to_owned()), + ); + spec.insert( + "locality".to_owned(), + serde_json::Value::String( + match intent.placement() { + BindingChildPlacement::Host => "host-local", + BindingChildPlacement::Guest => "guest-local", + } + .to_owned(), + ), + ); + spec.insert( + "visibility".to_owned(), + serde_json::Value::String("provider".to_owned()), + ); + spec.insert( + "attachmentPolicy".to_owned(), + serde_json::json!({ + "supported": true, + "maxAttachments": 1 + }), + ); + spec.insert( + "consumerPolicy".to_owned(), + serde_json::json!({ + "allowedOperations": ["resolve", "attach", "observe"] + }), + ); + spec.insert( + "lifecyclePolicy".to_owned(), + serde_json::Value::String("recycle-with-producer".to_owned()), + ); + } + } + let value = serde_json::json!({ + "apiVersion": "resources.d2bus.org/v3", + "type": intent.kind().resource_type(), + "metadata": { + "name": intent.resource_ref().name().as_str(), + "zone": zone.as_str(), + "ownerRef": owner_ref, + "finalizers": [], + "deletionRequestedAt": null, + "createdAt": "1970-01-01T00:00:00.000Z", + "updatedAt": "1970-01-01T00:00:00.000Z", + "generation": 1, + "revision": 1, + "managedBy": "controller" + }, + "spec": spec, + "status": { + "observedGeneration": 0, + "phase": "Pending", + "conditions": [], + "lastReconciledAt": null, + "startedAt": null, + "completedAt": null, + "outcome": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": null, + "observedGeneration": 0, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + }, + "resource": {} + } + }); + let bytes = serde_json::to_vec(&value) + .map_err(|_| BindingChildMaterializationError::MalformedResource)?; + let canonical = CanonicalJsonValue::parse(&bytes) + .map_err(|_| BindingChildMaterializationError::MalformedResource)? + .to_canonical_bytes(); + if canonical != bytes { + return Err(BindingChildMaterializationError::NonCanonicalResource); + } + Ok(canonical) +} + +/// Core-owned child reconciler backed by the durable owner index. +pub struct BindingChildReconciler { + owner_index: OwnerIndex, +} + +impl BindingChildReconciler { + /// Construct a bounded reconciler. + pub fn new(limits: OwnerLimits) -> Self { + Self { + owner_index: OwnerIndex::new(limits), + } + } + + /// Replace an owner's complete authoritative child relist. + pub fn relist( + &mut self, + owner: HintTarget, + observed: Vec, + ) -> Result<(), OwnerReconcileError> { + self.owner_index.relist(owner, observed) + } + + /// Plan create, repair, and ordered deletion mutations for one Binding. + /// + /// The complete child set must be supplied by the Provider controller; + /// omitted children are rejected rather than interpreted as permission to + /// delete them. Deletion after a later complete relist is handled by the + /// owner index with exact UID and revision preconditions. + pub fn plan( + &self, + owner: &HintTarget, + child_set: &BindingChildSet, + resources: &[BindingChildResource], + ) -> Result { + if owner.resource_ref() != child_set.owner_ref() { + return Err(BindingChildMaterializationError::OwnerMismatch); + } + let expected = child_set.resource_refs().cloned().collect::>(); + let supplied = resources + .iter() + .map(|resource| resource.intent().resource_ref().clone()) + .collect::>(); + if expected != supplied || supplied.len() != resources.len() { + return Err(BindingChildMaterializationError::IncompleteChildSet); + } + let desired = resources + .iter() + .map(|resource| { + let bundle_resource: BundleResource = + serde_json::from_slice(resource.canonical_resource()) + .map_err(|_| BindingChildMaterializationError::MalformedResource)?; + let zone = bundle_resource.metadata().zone().clone(); + if &zone != owner.zone() { + return Err(BindingChildMaterializationError::OwnerMismatch); + } + let payload = resource.create_payload(&zone)?; + let digest = semantic_child_digest(&payload)?; + DesiredChild::new(resource.intent().resource_ref().clone(), payload, digest) + .map_err(BindingChildMaterializationError::OwnerReconcile) + }) + .collect::, _>>()?; + self.owner_index + .plan(owner, desired) + .map_err(BindingChildMaterializationError::OwnerReconcile) + } + + /// Plan directly from Provider-declared UID-free intents. + /// + /// Core materializes every child body from the closed intent contract. + /// Providers never submit an arbitrary child envelope and callers do not + /// need to manufacture a second bundle representation merely to enter the + /// Resource API reconciliation path. + pub fn plan_intents( + &self, + owner: &HintTarget, + child_set: &BindingChildSet, + ) -> Result { + if owner.resource_ref() != child_set.owner_ref() { + return Err(BindingChildMaterializationError::OwnerMismatch); + } + let desired = child_set + .iter() + .map(|intent| { + let payload = materialize_child_create_payload(intent, owner.zone())?; + let digest = semantic_child_digest(&payload)?; + DesiredChild::new(intent.resource_ref().clone(), payload, digest) + .map_err(BindingChildMaterializationError::OwnerReconcile) + }) + .collect::, _>>()?; + self.owner_index + .plan(owner, desired) + .map_err(BindingChildMaterializationError::OwnerReconcile) + } + + /// Plan deletion of every currently indexed child. + pub fn plan_empty( + &self, + owner: &HintTarget, + ) -> Result { + self.owner_index + .plan(owner, Vec::new()) + .map_err(BindingChildMaterializationError::OwnerReconcile) + } + + /// Return the indexed child count after the latest complete relist. + pub fn child_count(&self, owner: &HintTarget) -> usize { + self.owner_index.child_count(owner) + } +} + +fn validate_spec( + intent: &BindingChildIntent, + spec: &d2b_contracts_resource::v3::CanonicalJsonObject, +) -> Result<(), BindingChildMaterializationError> { + match intent.kind() { + BindingChildKind::Process | BindingChildKind::EphemeralProcess => { + let Some(execution_ref) = spec.get("executionRef") else { + return Err(BindingChildMaterializationError::ExecutionTargetMismatch); + }; + if execution_ref + != &CanonicalJsonValue::String(intent.execution_ref().to_canonical_string()) + { + return Err(BindingChildMaterializationError::ExecutionTargetMismatch); + } + if let Some(provider) = intent.process_provider() { + if spec.get("providerRef") != Some(&CanonicalJsonValue::String(provider.to_owned())) + { + return Err(BindingChildMaterializationError::ProcessContractMismatch); + } + } + if let Some(template) = intent.process_template() { + if spec.get("template") != Some(&CanonicalJsonValue::String(template.to_owned())) { + return Err(BindingChildMaterializationError::ProcessContractMismatch); + } + } + if let Some(class) = intent.process_class() { + if spec.get("processClass") != Some(&CanonicalJsonValue::String(class.to_owned())) { + return Err(BindingChildMaterializationError::ProcessContractMismatch); + } + } + if let Some(domain) = intent.process_domain() { + let expected = match domain { + d2b_contracts_resource::v3::ExecutionDomain::System => "system", + d2b_contracts_resource::v3::ExecutionDomain::User => "user", + }; + if spec.get("domain") != Some(&CanonicalJsonValue::String(expected.to_owned())) { + return Err(BindingChildMaterializationError::ProcessContractMismatch); + } + } + if let Some(user_ref) = intent.process_user() { + if spec.get("userRef") + != Some(&CanonicalJsonValue::String(user_ref.to_canonical_string())) + { + return Err(BindingChildMaterializationError::ProcessContractMismatch); + } + } + } + BindingChildKind::Endpoint => { + let Some(producer_ref) = intent.producer_ref() else { + return Ok(()); + }; + let Some(actual) = spec.get("producerRef") else { + return Err(BindingChildMaterializationError::ProducerMismatch); + }; + if actual != &CanonicalJsonValue::String(producer_ref.to_canonical_string()) { + return Err(BindingChildMaterializationError::ProducerMismatch); + } + } + } + Ok(()) +} + +/// Errors from Core child validation or owner planning. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BindingChildMaterializationError { + /// The desired body was empty. + EmptyResource, + /// The desired body was not a valid bundle resource. + MalformedResource, + /// Resource type or name differed from the UID-free intent. + IdentityMismatch, + /// The child did not name its Binding owner. + OwnerMismatch, + /// The Process execution target differed from the intent. + ExecutionTargetMismatch, + /// The Process provider, template, class, domain, or user differed from + /// the declared intent. + ProcessContractMismatch, + /// The Endpoint producer differed from the intent. + ProducerMismatch, + /// The complete child set was not supplied exactly once. + IncompleteChildSet, + /// The body was valid JSON but not canonical. + NonCanonicalResource, + /// The generic owner planner rejected the desired/observed set. + OwnerReconcile(OwnerReconcileError), +} + +impl core::fmt::Display for BindingChildMaterializationError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(match self { + Self::EmptyResource => "binding-child-resource-empty", + Self::MalformedResource => "binding-child-resource-malformed", + Self::IdentityMismatch => "binding-child-resource-identity-mismatch", + Self::OwnerMismatch => "binding-child-resource-owner-mismatch", + Self::ExecutionTargetMismatch => "binding-child-resource-execution-mismatch", + Self::ProcessContractMismatch => "binding-child-resource-process-contract-mismatch", + Self::ProducerMismatch => "binding-child-resource-producer-mismatch", + Self::IncompleteChildSet => "binding-child-resource-set-incomplete", + Self::NonCanonicalResource => "binding-child-resource-not-canonical", + Self::OwnerReconcile(error) => return write!(formatter, "{error}"), + }) + } +} + +impl std::error::Error for BindingChildMaterializationError {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::OwnerMutation; + use d2b_contracts_provider::v3::semantic_services::{ + SemanticFamily, + child_resources::{ + BindingChildKind, BindingChildPlacement, BindingChildRequest, explicit_binding_children, + }, + }; + use d2b_contracts_resource::v3::{ + CanonicalJsonObject, ResourceEnvelope, ResourceUid, ZoneId, ZoneRevision, + }; + use d2b_contracts_zone_session::v3::resource_bundle::BundleResourceMetadata; + use std::collections::BTreeMap; + + fn child_set() -> BindingChildSet { + explicit_binding_children( + SemanticFamily::Audio, + ResourceRef::parse("audio.d2bus.org.AudioBinding/microphone").unwrap(), + ResourceRef::parse("audio.d2bus.org.AudioService/host").unwrap(), + ResourceRef::parse("Guest/dev-vm").unwrap(), + ResourceRef::parse("Provider/audio-pipewire").unwrap(), + &[ + BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Guest, + "guest-agent", + ), + BindingChildRequest::endpoint( + BindingChildPlacement::Guest, + "guest-endpoint", + "guest-agent", + ), + ], + ) + .unwrap() + } + + fn owner(set: &BindingChildSet) -> HintTarget { + HintTarget::new( + ZoneId::parse("dev").unwrap(), + set.owner_ref().clone(), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ) + } + + fn child_resource(intent: &BindingChildIntent) -> BindingChildResource { + let mut fields = BTreeMap::new(); + match intent.kind() { + BindingChildKind::Process | BindingChildKind::EphemeralProcess => { + fields.insert( + "executionRef".to_owned(), + CanonicalJsonValue::String(intent.execution_ref().to_canonical_string()), + ); + } + BindingChildKind::Endpoint => { + fields.insert( + "producerRef".to_owned(), + CanonicalJsonValue::String( + intent.producer_ref().unwrap().to_canonical_string(), + ), + ); + } + } + let spec = CanonicalJsonObject::parse( + &serde_json::to_vec(&CanonicalJsonValue::Object(fields)).unwrap(), + ) + .unwrap(); + let metadata = BundleResourceMetadata::new( + intent.resource_ref().name().clone(), + ZoneId::parse("dev").unwrap(), + Some(intent.owner_ref().clone()), + BTreeMap::new(), + BTreeMap::new(), + ); + let resource = BundleResource::new( + ResourceTypeName::parse(intent.kind().resource_type()).unwrap(), + metadata, + spec, + ) + .unwrap(); + let bytes = CanonicalJsonValue::parse(&serde_json::to_vec(&resource).unwrap()) + .unwrap() + .to_canonical_bytes(); + BindingChildResource::new(intent.clone(), bytes).unwrap() + } + + #[test] + fn validates_child_identity_owner_and_execution_contract() { + let set = child_set(); + let intent = set.child("guest-agent").unwrap(); + let resource = child_resource(intent); + assert_eq!(resource.intent().resource_ref(), intent.resource_ref()); + assert_eq!( + resource.intent().execution_ref(), + &ResourceRef::parse("Guest/dev-vm").unwrap() + ); + + let mut invalid = + serde_json::from_slice::(resource.canonical_resource()).unwrap(); + invalid["spec"]["executionRef"] = serde_json::Value::String("Guest/other".to_owned()); + let bytes = CanonicalJsonValue::parse(&serde_json::to_vec(&invalid).unwrap()) + .unwrap() + .to_canonical_bytes(); + assert_eq!( + BindingChildResource::new(intent.clone(), bytes), + Err(BindingChildMaterializationError::ExecutionTargetMismatch) + ); + } + + #[test] + fn rejects_incomplete_child_sets_and_reconciles_all_declared_children() { + let set = child_set(); + let process = child_resource(set.child("guest-agent").unwrap()); + let endpoint = child_resource(set.child("guest-endpoint").unwrap()); + let mut reconciler = BindingChildReconciler::new(OwnerLimits::new(4, 8).unwrap()); + let parent = owner(&set); + reconciler.relist(parent.clone(), Vec::new()).unwrap(); + + assert_eq!( + reconciler.plan(&parent, &set, std::slice::from_ref(&process)), + Err(BindingChildMaterializationError::IncompleteChildSet) + ); + let plan = reconciler + .plan(&parent, &set, &[process, endpoint]) + .unwrap(); + assert_eq!(plan.mutations().len(), 2); + assert!( + plan.mutations() + .iter() + .all(|mutation| matches!(mutation, OwnerMutation::Create { .. })) + ); + } + + #[test] + fn plans_uid_free_intents_without_provider_supplied_envelopes() { + let set = child_set(); + let mut reconciler = BindingChildReconciler::new(OwnerLimits::new(4, 8).unwrap()); + let parent = owner(&set); + reconciler.relist(parent.clone(), Vec::new()).unwrap(); + + let plan = reconciler.plan_intents(&parent, &set).unwrap(); + assert_eq!(plan.mutations().len(), 2); + assert!(plan.mutations().iter().all(|mutation| matches!( + mutation, + OwnerMutation::Create { canonical_resource, .. } + if !canonical_resource.is_empty() + ))); + let endpoint_payload = plan + .mutations() + .iter() + .find_map(|mutation| match mutation { + OwnerMutation::Create { + target, + canonical_resource, + } if target.resource_type().as_str() == "Endpoint" => Some(canonical_resource), + _ => None, + }) + .unwrap(); + let CanonicalJsonValue::Object(root) = + CanonicalJsonValue::parse(endpoint_payload).unwrap() + else { + unreachable!() + }; + let endpoint_spec = root.get("spec").unwrap(); + let endpoint_spec = + serde_json::from_slice::( + &endpoint_spec.to_canonical_bytes(), + ) + .unwrap(); + assert_eq!( + endpoint_spec + .provider_ref() + .unwrap() + .to_canonical_string(), + "Provider/audio-pipewire" + ); + assert!(endpoint_spec.base().get("providerRef").is_none()); + } + + #[test] + fn preserves_uid_revision_preconditions_for_repair_and_deletion() { + let set = child_set(); + let process = child_resource(set.child("guest-agent").unwrap()); + let endpoint = child_resource(set.child("guest-endpoint").unwrap()); + let endpoint_payload = endpoint + .create_payload(&ZoneId::parse("dev").unwrap()) + .unwrap(); + let mut reconciler = BindingChildReconciler::new(OwnerLimits::new(4, 8).unwrap()); + let parent = owner(&set); + let observed = vec![ + ObservedChild::new( + HintTarget::new( + ZoneId::parse("dev").unwrap(), + process.intent().resource_ref().clone(), + ResourceUid::parse("223e4567-e89b-42d3-a456-426614174000").unwrap(), + ), + ZoneRevision::new(3), + "sha256:stale", + false, + ) + .unwrap(), + ObservedChild::new( + HintTarget::new( + ZoneId::parse("dev").unwrap(), + endpoint.intent().resource_ref().clone(), + ResourceUid::parse("323e4567-e89b-42d3-a456-426614174000").unwrap(), + ), + ZoneRevision::new(4), + semantic_child_digest(&endpoint_payload).unwrap(), + false, + ) + .unwrap(), + ]; + reconciler.relist(parent.clone(), observed).unwrap(); + + let repair = reconciler + .plan(&parent, &set, &[process.clone(), endpoint.clone()]) + .unwrap(); + assert_eq!( + repair + .mutations() + .iter() + .filter(|mutation| matches!(mutation, OwnerMutation::Repair { .. })) + .count(), + 1 + ); + assert!(repair.mutations().iter().any(|mutation| { + matches!( + mutation, + OwnerMutation::Repair { + expected_revision, .. + } if *expected_revision == ZoneRevision::new(3) + ) + })); + + let reduced = explicit_binding_children( + SemanticFamily::Audio, + set.owner_ref().clone(), + ResourceRef::parse("audio.d2bus.org.AudioService/host").unwrap(), + set.target_ref().clone(), + ResourceRef::parse("Provider/audio-pipewire").unwrap(), + &[BindingChildRequest::new( + BindingChildKind::Process, + BindingChildPlacement::Guest, + "guest-agent", + )], + ) + .unwrap(); + let deletion = reconciler.plan(&parent, &reduced, &[process]).unwrap(); + assert!(deletion.mutations().iter().any(|mutation| { + matches!( + mutation, + OwnerMutation::RequestDeletion { target, expected_revision, .. } + if target == set.child("guest-endpoint").unwrap().resource_ref() + && *expected_revision == ZoneRevision::new(4) + ) + })); + } + + #[test] + fn materializes_store_create_payloads_with_process_provider_and_endpoint_contract() { + let set = child_set(); + let process_intent = set.child("guest-agent").unwrap(); + let endpoint_intent = set.child("guest-endpoint").unwrap(); + let process = BindingChildResource::new( + process_intent.clone(), + child_resource(process_intent).canonical_resource().to_vec(), + ) + .unwrap(); + let process_payload = process + .create_payload(&ZoneId::parse("dev").unwrap()) + .unwrap(); + let mut process_value: serde_json::Value = + serde_json::from_slice(&process_payload).unwrap(); + process_value["metadata"]["uid"] = + serde_json::Value::String("123e4567-e89b-42d3-a456-426614174000".to_owned()); + let process_bytes = serde_json::to_vec(&process_value).unwrap(); + let process_envelope = ResourceEnvelope::from_json(&process_bytes).unwrap(); + assert_eq!( + process_envelope + .spec() + .provider_ref() + .unwrap() + .to_canonical_string(), + "Provider/system-systemd" + ); + assert_eq!( + process_envelope.spec().base().get("executionRef"), + Some(&CanonicalJsonValue::String("Guest/dev-vm".to_owned())) + ); + + let endpoint = BindingChildResource::new( + endpoint_intent.clone(), + child_resource(endpoint_intent) + .canonical_resource() + .to_vec(), + ) + .unwrap(); + let endpoint_payload = endpoint + .create_payload(&ZoneId::parse("dev").unwrap()) + .unwrap(); + let mut endpoint_value: serde_json::Value = + serde_json::from_slice(&endpoint_payload).unwrap(); + endpoint_value["metadata"]["uid"] = + serde_json::Value::String("223e4567-e89b-42d3-a456-426614174000".to_owned()); + let endpoint_envelope = + ResourceEnvelope::from_json(&serde_json::to_vec(&endpoint_value).unwrap()).unwrap(); + assert_eq!( + endpoint_envelope.spec().base().get("producerRef"), + Some(&CanonicalJsonValue::String( + endpoint_intent + .producer_ref() + .unwrap() + .to_canonical_string() + )) + ); + } + + #[test] + fn semantic_digest_ignores_store_runtime_fields() { + let set = child_set(); + let child = child_resource(set.child("guest-agent").unwrap()); + let payload = child + .create_payload(&ZoneId::parse("dev").unwrap()) + .unwrap(); + let mut stored: serde_json::Value = serde_json::from_slice(&payload).unwrap(); + stored["metadata"]["uid"] = + serde_json::Value::String("423e4567-e89b-42d3-a456-426614174000".to_owned()); + stored["metadata"]["revision"] = serde_json::Value::from(91_u64); + stored["metadata"]["updatedAt"] = + serde_json::Value::String("2026-08-22T02:00:00.000Z".to_owned()); + stored["status"]["phase"] = serde_json::Value::String("Ready".to_owned()); + let stored_bytes = serde_json::to_vec(&stored).unwrap(); + assert_eq!( + semantic_child_digest(&payload).unwrap(), + semantic_child_digest(&stored_bytes).unwrap() + ); + } + + #[test] + fn create_payload_rejects_a_cross_zone_submission() { + let set = child_set(); + let child = child_resource(set.child("guest-agent").unwrap()); + assert_eq!( + child.create_payload(&ZoneId::parse("other").unwrap()), + Err(BindingChildMaterializationError::OwnerMismatch) + ); + } + + #[test] + fn plan_rejects_a_provider_body_from_a_foreign_zone() { + let set = child_set(); + let intent = set.child("guest-agent").unwrap(); + let child = child_resource(intent); + let mut value: serde_json::Value = + serde_json::from_slice(child.canonical_resource()).unwrap(); + value["metadata"]["zone"] = serde_json::Value::String("other".to_owned()); + let bytes = CanonicalJsonValue::parse(&serde_json::to_vec(&value).unwrap()) + .unwrap() + .to_canonical_bytes(); + let foreign_child = BindingChildResource::new(intent.clone(), bytes).unwrap(); + let endpoint = child_resource(set.child("guest-endpoint").unwrap()); + let mut reconciler = BindingChildReconciler::new(OwnerLimits::new(4, 8).unwrap()); + let parent = owner(&set); + reconciler.relist(parent.clone(), Vec::new()).unwrap(); + + assert_eq!( + reconciler.plan(&parent, &set, &[foreign_child, endpoint]), + Err(BindingChildMaterializationError::OwnerMismatch) + ); + } +} diff --git a/packages/d2b-core-controller/src/cleanup.rs b/packages/d2b-core-controller/src/cleanup.rs index c8b069ed51..159a2aa2bd 100644 --- a/packages/d2b-core-controller/src/cleanup.rs +++ b/packages/d2b-core-controller/src/cleanup.rs @@ -7,11 +7,7 @@ use std::collections::{BTreeMap, BTreeSet}; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - ResourceTypeName, - Timestamp, - ZoneRevision, + ConfigurationGeneration, ResourceBundleGenerationId, ResourceTypeName, Timestamp, ZoneRevision, execution_policy::DurationMs, }; @@ -949,10 +945,7 @@ fn civil_from_days(days: i128) -> (i128, i128, i128) { #[cfg(test)] mod ephemeral_cleanup_tests { use super::*; - use d2b_contracts_resource::v3::{ - ResourceName, - ResourceTypeName, -}; + use d2b_contracts_resource::v3::{ResourceName, ResourceTypeName}; fn key() -> ResourceKey { ResourceKey::new( diff --git a/packages/d2b-core-controller/src/configuration/bundle_apply.rs b/packages/d2b-core-controller/src/configuration/bundle_apply.rs index 58cbff7fe5..094c899c7b 100644 --- a/packages/d2b-core-controller/src/configuration/bundle_apply.rs +++ b/packages/d2b-core-controller/src/configuration/bundle_apply.rs @@ -7,14 +7,9 @@ use std::collections::{BTreeMap, BTreeSet}; +use d2b_contracts_resource::v3::{ConfigurationGeneration, Timestamp}; use d2b_contracts_zone_session::v3::{ - BundleMetadata, - BundleResource as InputBundleResource, - ZoneBundle, -}; -use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - Timestamp, + BundleMetadata, BundleResource as InputBundleResource, ZoneBundle, }; use super::{ @@ -617,18 +612,11 @@ fn persist_effect( #[cfg(test)] mod tests { use super::*; - use d2b_contracts_zone_session::v3::{ - BundleMetadata, - BundleResource as InputBundleResource, -}; use d2b_contracts_resource::v3::{ - CanonicalJsonObject, - ResourceBundleGenerationId, - ResourceName, - ResourceTypeName, - SchemaFingerprint, - ZoneId, -}; + CanonicalJsonObject, ResourceBundleGenerationId, ResourceName, ResourceTypeName, + SchemaFingerprint, ZoneId, + }; + use d2b_contracts_zone_session::v3::{BundleMetadata, BundleResource as InputBundleResource}; fn digest(byte: char) -> SchemaFingerprint { SchemaFingerprint::parse(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() diff --git a/packages/d2b-core-controller/src/configuration/generation_transition.rs b/packages/d2b-core-controller/src/configuration/generation_transition.rs index d9aef3736d..2cada9c745 100644 --- a/packages/d2b-core-controller/src/configuration/generation_transition.rs +++ b/packages/d2b-core-controller/src/configuration/generation_transition.rs @@ -6,13 +6,10 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_zone_session::v3::ZoneBundle; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - SchemaFingerprint, - Timestamp, + ConfigurationGeneration, ResourceBundleGenerationId, SchemaFingerprint, Timestamp, }; +use d2b_contracts_zone_session::v3::ZoneBundle; use crate::resource_store::{ ManagedBy, PersistedResourceMetadata, PersistedResourceRecord, ResourceMetadataError, diff --git a/packages/d2b-core-controller/src/configuration/mod.rs b/packages/d2b-core-controller/src/configuration/mod.rs index ba5bad2d4b..c5af36ec5b 100644 --- a/packages/d2b-core-controller/src/configuration/mod.rs +++ b/packages/d2b-core-controller/src/configuration/mod.rs @@ -55,18 +55,11 @@ pub mod generation_transition; use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_zone_session::v3::ZoneBundle; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - ResourceName, - ResourceTypeName, - ResourceUid, - SchemaFingerprint, - Timestamp, - ZoneId, - ZoneRevision, + ConfigurationGeneration, ResourceBundleGenerationId, ResourceName, ResourceTypeName, + ResourceUid, SchemaFingerprint, Timestamp, ZoneId, ZoneRevision, }; +use d2b_contracts_zone_session::v3::ZoneBundle; use crate::{ audit::{AuditError, AuditEvent, AuditLedger, AuditReason}, diff --git a/packages/d2b-core-controller/src/controller_assignment.rs b/packages/d2b-core-controller/src/controller_assignment.rs new file mode 100644 index 0000000000..46d72cd7b6 --- /dev/null +++ b/packages/d2b-core-controller/src/controller_assignment.rs @@ -0,0 +1,2039 @@ +//! Single-owner Provider controller assignment and fenced ResourceClient leases. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt, + sync::{ + Arc, + atomic::{AtomicBool, AtomicU8, Ordering}, + }, +}; + +use d2b_contracts_provider::v3::{ + ComponentType, ControllerInstanceScope, ControllerTargetKind, ProviderManifest, +}; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; +use d2b_contracts_resource::v3::{ + CanonicalJsonValue, ControllerGeneration, PlacementAnchor, PlacementTarget, + PlacementTargetKind, ResourceEnvelope, ResourceGeneration, ResourceName, ResourceRef, + ResourceTypeName, ResourceUid, ZoneId, ZoneRevision, +}; +use serde_json::{Map, Value, json}; + +/// Maximum encoded assignment evidence carried by one scoped commit. +pub const MAX_SCOPED_COMMIT_TRANSPORT_BYTES: usize = 64 * 1024; + +/// The maximum number of assignments held by one Zone authority. +pub const MAX_ASSIGNMENTS: usize = 16_384; +/// Maximum child ownership entries retained by one assignment. +pub const MAX_ASSIGNED_CHILDREN: usize = 4_096; + +/// A monotonically increasing, nonzero assignment epoch. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssignmentEpoch(u64); + +impl AssignmentEpoch { + /// Construct a nonzero epoch. + pub fn new(value: u64) -> Result { + if value == 0 { + return Err(AssignmentError::EpochExhausted); + } + Ok(Self(value)) + } + + /// Return the opaque epoch ordinal. + pub const fn get(self) -> u64 { + self.0 + } +} + +impl fmt::Debug for AssignmentEpoch { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("AssignmentEpoch()") + } +} + +/// The exact target selected by a contract-owned placement anchor. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AssignmentTarget { + /// The Zone singleton target. + Zone(ZoneId), + /// One exact Host or Guest execution target. + Execution { + /// The target kind. + kind: PlacementTargetKind, + /// The exact target reference. + reference: ResourceRef, + }, +} + +impl AssignmentTarget { + fn from_placement(target: PlacementTarget) -> Self { + match target { + PlacementTarget::Zone(zone) => Self::Zone(zone), + PlacementTarget::Execution { kind, reference } => Self::Execution { kind, reference }, + } + } + + fn target_kind(&self) -> Option { + match self { + Self::Zone(_) => Some(ControllerTargetKind::Zone), + Self::Execution { + kind: PlacementTargetKind::Host, + .. + } => Some(ControllerTargetKind::Host), + Self::Execution { + kind: PlacementTargetKind::Guest, + .. + } => Some(ControllerTargetKind::Guest), + } + } + + /// Borrow the exact execution target reference, when present. + pub fn execution_ref(&self) -> Option<&ResourceRef> { + match self { + Self::Zone(_) => None, + Self::Execution { reference, .. } => Some(reference), + } + } +} + +impl fmt::Debug for AssignmentTarget { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Zone(_) => formatter.write_str("AssignmentTarget::Zone()"), + Self::Execution { kind, .. } => formatter + .debug_struct("AssignmentTarget::Execution") + .field("kind", kind) + .finish_non_exhaustive(), + } + } +} + +/// One resource-plane operation a controller lease may perform. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AssignmentVerb { + Get, + List, + Watch, + Create, + UpdateSpec, + UpdateStatus, + UpdateMetadata, + UpdateFinalizers, + Delete, + CommitBatch, +} + +impl AssignmentVerb { + /// Whether this operation can mutate durable resource state. + pub const fn is_mutating(self) -> bool { + !matches!(self, Self::Get | Self::List | Self::Watch) + } +} + +/// Lifecycle of one resource's controller assignment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AssignmentPhase { + Pending, + Assigned, + Draining, + Revoked, + Stale, + Quarantined, + Released, +} + +impl AssignmentPhase { + const fn code(self) -> u8 { + match self { + Self::Pending => 0, + Self::Assigned => 1, + Self::Draining => 2, + Self::Revoked => 3, + Self::Stale => 4, + Self::Quarantined => 5, + Self::Released => 6, + } + } + + fn from_code(value: u8) -> Self { + match value { + 1 => Self::Assigned, + 2 => Self::Draining, + 3 => Self::Revoked, + 4 => Self::Stale, + 5 => Self::Quarantined, + 6 => Self::Released, + _ => Self::Pending, + } + } + + const fn admits_watch(self) -> bool { + matches!(self, Self::Assigned) + } + + const fn admits_mutation(self) -> bool { + matches!(self, Self::Assigned) + } +} + +/// Closed assignment or lease failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AssignmentError { + InvalidRole, + ProviderGenerationMismatch, + ControllerGenerationMismatch, + SessionGenerationInvalid, + SessionBindingMismatch, + ResourceTypeUnowned, + PlacementAnchorMissing, + PlacementTargetInvalid, + TargetKindUnsupported, + TargetNotReady, + AssignmentConflict, + AssignmentLimit, + AssignmentMissing, + AssignmentNotDraining, + AssignmentNotReleased, + ChildrenRemain, + ChildLimit, + StaleAssignment, + SessionRevoked, + ResourceRevisionMismatch, + ResourceUidMismatch, + ResourceNotAssigned, + TargetMismatch, + VerbNotAllowed, + QueryWidened, + EpochExhausted, + RoleContractInvalid, +} + +/// Failure while decoding the assignment evidence carried by the existing +/// Resource CommitBatch transport. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AssignmentTransportError { + Malformed, + TooLarge, +} + +impl AssignmentTransportError { + /// Return the stable identity-free reason code. + pub const fn code(self) -> &'static str { + match self { + Self::Malformed => "assignment-transport-malformed", + Self::TooLarge => "assignment-transport-too-large", + } + } +} + +impl fmt::Display for AssignmentTransportError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.code()) + } +} + +impl std::error::Error for AssignmentTransportError {} + +impl AssignmentError { + /// Return the stable identity-free reason code. + pub const fn code(self) -> &'static str { + match self { + Self::InvalidRole => "assignment-role-invalid", + Self::ProviderGenerationMismatch => "assignment-provider-generation-mismatch", + Self::ControllerGenerationMismatch => "assignment-controller-generation-mismatch", + Self::SessionGenerationInvalid => "assignment-session-generation-invalid", + Self::SessionBindingMismatch => "assignment-session-binding-mismatch", + Self::ResourceTypeUnowned => "assignment-resource-type-unowned", + Self::PlacementAnchorMissing => "assignment-placement-anchor-missing", + Self::PlacementTargetInvalid => "assignment-placement-target-invalid", + Self::TargetKindUnsupported => "assignment-target-kind-unsupported", + Self::TargetNotReady => "assignment-target-not-ready", + Self::AssignmentConflict => "assignment-conflict", + Self::AssignmentLimit => "assignment-limit", + Self::AssignmentMissing => "assignment-missing", + Self::AssignmentNotDraining => "assignment-not-draining", + Self::AssignmentNotReleased => "assignment-not-released", + Self::ChildrenRemain => "assignment-children-remain", + Self::ChildLimit => "assignment-child-limit", + Self::StaleAssignment => "assignment-stale", + Self::SessionRevoked => "assignment-session-revoked", + Self::ResourceRevisionMismatch => "assignment-resource-revision-mismatch", + Self::ResourceUidMismatch => "assignment-resource-uid-mismatch", + Self::ResourceNotAssigned => "assignment-resource-not-assigned", + Self::TargetMismatch => "assignment-target-mismatch", + Self::VerbNotAllowed => "assignment-verb-not-allowed", + Self::QueryWidened => "assignment-query-widened", + Self::EpochExhausted => "assignment-epoch-exhausted", + Self::RoleContractInvalid => "assignment-role-contract-invalid", + } + } +} + +impl fmt::Display for AssignmentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.code()) + } +} + +impl std::error::Error for AssignmentError {} + +/// An immutable assignment identity carried by every controller admission. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssignmentIdentity { + resource_uid: ResourceUid, + resource_revision: ZoneRevision, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + controller_role: ResourceRef, + target: AssignmentTarget, + session_generation: ReconnectGeneration, + epoch: AssignmentEpoch, +} + +impl AssignmentIdentity { + /// Construct one identity from authoritative committed values. + #[allow(clippy::too_many_arguments)] + fn new( + resource_uid: ResourceUid, + resource_revision: ZoneRevision, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + controller_role: ResourceRef, + target: AssignmentTarget, + session_generation: ReconnectGeneration, + epoch: AssignmentEpoch, + ) -> Self { + Self { + resource_uid, + resource_revision, + provider_generation, + controller_generation, + controller_role, + target, + session_generation, + epoch, + } + } +} + +/// Assignment and mutation evidence forwarded through the existing Resource +/// CommitBatch RPC after bus admission. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedCommitTransport { + assignment: AssignmentIdentity, + mutations: Vec, +} + +impl ScopedCommitTransport { + /// Construct transport evidence from one admitted assignment call. + pub fn new( + assignment: AssignmentIdentity, + mutations: Vec, + ) -> Result { + if mutations.is_empty() + || mutations.len() > 128 + || mutations.iter().any(|mutation| { + mutation.assignment() != &assignment + || !mutation.verb().is_mutating() + || mutation.verb() == AssignmentVerb::CommitBatch + }) + { + return Err(AssignmentTransportError::Malformed); + } + Ok(Self { + assignment, + mutations, + }) + } + + /// Borrow the admitted assignment. + pub const fn assignment(&self) -> &AssignmentIdentity { + &self.assignment + } + + /// Borrow the admitted mutations. + pub fn mutations(&self) -> &[ScopedResourceMutation] { + &self.mutations + } + + /// Encode the evidence as bounded canonical JSON bytes. + pub fn encode(&self) -> Result, AssignmentTransportError> { + let value = json!({ + "version": 1, + "assignment": encode_assignment(&self.assignment), + "mutations": self + .mutations + .iter() + .map(encode_mutation) + .collect::>(), + }); + let bytes = serde_json::to_vec(&value).map_err(|_| AssignmentTransportError::Malformed)?; + let bytes = CanonicalJsonValue::parse(&bytes) + .map_err(|_| AssignmentTransportError::Malformed)? + .to_canonical_bytes(); + if bytes.len() > MAX_SCOPED_COMMIT_TRANSPORT_BYTES { + return Err(AssignmentTransportError::TooLarge); + } + Ok(bytes) + } + + /// Decode bounded evidence produced by [`Self::encode`]. + pub fn decode(bytes: &[u8]) -> Result { + if bytes.is_empty() || bytes.len() > MAX_SCOPED_COMMIT_TRANSPORT_BYTES { + return Err(AssignmentTransportError::TooLarge); + } + CanonicalJsonValue::parse(bytes).map_err(|_| AssignmentTransportError::Malformed)?; + let value = serde_json::from_slice::(bytes) + .map_err(|_| AssignmentTransportError::Malformed)?; + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + require_exact_keys(object, &["version", "assignment", "mutations"])?; + if object.get("version").and_then(Value::as_u64) != Some(1) { + return Err(AssignmentTransportError::Malformed); + } + let assignment = decode_assignment( + object + .get("assignment") + .ok_or(AssignmentTransportError::Malformed)?, + )?; + let mutation_values = object + .get("mutations") + .and_then(Value::as_array) + .ok_or(AssignmentTransportError::Malformed)?; + if mutation_values.is_empty() || mutation_values.len() > 128 { + return Err(AssignmentTransportError::Malformed); + } + let mutations = mutation_values + .iter() + .map(|value| { + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + require_exact_keys(object, &["target", "verb"])?; + let target = ResourceRef::parse( + object + .get("target") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?; + let verb = decode_assignment_verb( + object + .get("verb") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + )?; + Ok(ScopedResourceMutation { + assignment: assignment.clone(), + target, + verb, + }) + }) + .collect::, _>>()?; + Self::new(assignment, mutations) + } +} + +impl fmt::Debug for ScopedCommitTransport { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedCommitTransport") + .field("mutation_count", &self.mutations.len()) + .finish() + } +} + +fn encode_assignment(identity: &AssignmentIdentity) -> Value { + let target = match identity.target() { + AssignmentTarget::Zone(zone) => json!({ + "kind": "zone", + "zone": zone.as_str(), + }), + AssignmentTarget::Execution { kind, reference } => json!({ + "kind": "execution", + "targetKind": match kind { + PlacementTargetKind::Host => "host", + PlacementTargetKind::Guest => "guest", + }, + "reference": reference.to_canonical_string(), + }), + }; + json!({ + "resourceUid": identity.resource_uid().as_str(), + "resourceRevision": identity.resource_revision().get(), + "providerGeneration": identity.provider_generation().get(), + "controllerGeneration": identity.controller_generation().get(), + "controllerRole": identity.controller_role().to_canonical_string(), + "target": target, + "sessionGeneration": identity.session_generation().get(), + "epoch": identity.epoch().get(), + }) +} + +fn encode_mutation(mutation: &ScopedResourceMutation) -> Value { + json!({ + "target": mutation.target().to_canonical_string(), + "verb": encode_assignment_verb(mutation.verb()), + }) +} + +fn encode_assignment_verb(verb: AssignmentVerb) -> &'static str { + match verb { + AssignmentVerb::Get => "Get", + AssignmentVerb::List => "List", + AssignmentVerb::Watch => "Watch", + AssignmentVerb::Create => "Create", + AssignmentVerb::UpdateSpec => "UpdateSpec", + AssignmentVerb::UpdateStatus => "UpdateStatus", + AssignmentVerb::UpdateMetadata => "UpdateMetadata", + AssignmentVerb::UpdateFinalizers => "UpdateFinalizers", + AssignmentVerb::Delete => "Delete", + AssignmentVerb::CommitBatch => "CommitBatch", + } +} + +fn decode_assignment_verb(value: &str) -> Result { + match value { + "Get" => Ok(AssignmentVerb::Get), + "List" => Ok(AssignmentVerb::List), + "Watch" => Ok(AssignmentVerb::Watch), + "Create" => Ok(AssignmentVerb::Create), + "UpdateSpec" => Ok(AssignmentVerb::UpdateSpec), + "UpdateStatus" => Ok(AssignmentVerb::UpdateStatus), + "UpdateMetadata" => Ok(AssignmentVerb::UpdateMetadata), + "UpdateFinalizers" => Ok(AssignmentVerb::UpdateFinalizers), + "Delete" => Ok(AssignmentVerb::Delete), + "CommitBatch" => Ok(AssignmentVerb::CommitBatch), + _ => Err(AssignmentTransportError::Malformed), + } +} + +fn decode_assignment(value: &Value) -> Result { + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + require_exact_keys( + object, + &[ + "resourceUid", + "resourceRevision", + "providerGeneration", + "controllerGeneration", + "controllerRole", + "target", + "sessionGeneration", + "epoch", + ], + )?; + let target = decode_assignment_target( + object + .get("target") + .ok_or(AssignmentTransportError::Malformed)?, + )?; + Ok(AssignmentIdentity::new( + ResourceUid::parse( + object + .get("resourceUid") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + ZoneRevision::new( + object + .get("resourceRevision") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ), + ResourceGeneration::new( + object + .get("providerGeneration") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + ControllerGeneration::new( + object + .get("controllerGeneration") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + ResourceRef::parse( + object + .get("controllerRole") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + target, + ReconnectGeneration::new( + object + .get("sessionGeneration") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + AssignmentEpoch::new( + object + .get("epoch") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + )) +} + +fn decode_assignment_target(value: &Value) -> Result { + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + let kind = object + .get("kind") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?; + match kind { + "zone" => { + require_exact_keys(object, &["kind", "zone"])?; + Ok(AssignmentTarget::Zone( + ZoneId::parse( + object + .get("zone") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + )) + } + "execution" => { + require_exact_keys(object, &["kind", "targetKind", "reference"])?; + let reference = ResourceRef::parse( + object + .get("reference") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?; + let target_kind = match object + .get("targetKind") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)? + { + "host" => PlacementTargetKind::Host, + "guest" => PlacementTargetKind::Guest, + _ => return Err(AssignmentTransportError::Malformed), + }; + if (target_kind == PlacementTargetKind::Host + && reference.resource_type().as_str() != "Host") + || (target_kind == PlacementTargetKind::Guest + && reference.resource_type().as_str() != "Guest") + { + return Err(AssignmentTransportError::Malformed); + } + Ok(AssignmentTarget::Execution { + kind: target_kind, + reference, + }) + } + _ => Err(AssignmentTransportError::Malformed), + } +} + +fn require_exact_keys( + object: &Map, + expected: &[&str], +) -> Result<(), AssignmentTransportError> { + if object.len() != expected.len() || expected.iter().any(|key| !object.contains_key(*key)) { + return Err(AssignmentTransportError::Malformed); + } + Ok(()) +} + +impl AssignmentIdentity { + /// Borrow the assigned resource UID. + pub const fn resource_uid(&self) -> &ResourceUid { + &self.resource_uid + } + + /// Return the committed resource revision bound by this identity. + pub const fn resource_revision(&self) -> ZoneRevision { + self.resource_revision + } + + /// Return the Provider generation bound by this identity. + pub const fn provider_generation(&self) -> ResourceGeneration { + self.provider_generation + } + + /// Return the Core controller generation bound by this identity. + pub const fn controller_generation(&self) -> ControllerGeneration { + self.controller_generation + } + + /// Borrow the signed controller role. + pub const fn controller_role(&self) -> &ResourceRef { + &self.controller_role + } + + /// Borrow the exact assigned target. + pub const fn target(&self) -> &AssignmentTarget { + &self.target + } + + /// Return the authenticated ComponentSession generation. + pub const fn session_generation(&self) -> ReconnectGeneration { + self.session_generation + } + + /// Return the assignment epoch. + pub const fn epoch(&self) -> AssignmentEpoch { + self.epoch + } +} + +impl fmt::Debug for AssignmentIdentity { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("AssignmentIdentity") + .field("resource_uid", &"") + .field("resource_revision", &self.resource_revision) + .field("provider_generation", &self.provider_generation) + .field("controller_generation", &self.controller_generation) + .field("controller_role", &"") + .field("target", &self.target) + .field("session_generation", &self.session_generation) + .field("epoch", &self.epoch) + .finish() + } +} + +/// The signed role and placement contract used for assignment admission. +#[derive(Clone, PartialEq, Eq)] +pub struct ControllerRoleContract { + provider_ref: ResourceRef, + role_ref: ResourceRef, + scope: ControllerInstanceScope, + supported_target_kinds: BTreeSet, + resource_types: BTreeSet, + placements: BTreeMap, +} + +impl ControllerRoleContract { + /// Derive one role contract from a trusted signed Provider manifest. + pub fn from_signed_manifest( + provider_ref: ResourceRef, + role_ref: ResourceRef, + manifest: &ProviderManifest, + ) -> Result { + if provider_ref.resource_type().as_str() != "Provider" + || role_ref.resource_type().as_str() != "Process" + || manifest.validate_installation_contract().is_err() + { + return Err(AssignmentError::InvalidRole); + } + let component = manifest + .components() + .iter() + .find(|component| { + component.component_type() == ComponentType::Controller + && component.component_id().as_str() == role_ref.name().as_str() + }) + .ok_or(AssignmentError::InvalidRole)?; + let scope = component + .instance_scope() + .ok_or(AssignmentError::RoleContractInvalid)?; + let mut placements = BTreeMap::new(); + for resource_type in component.exported_resource_types() { + let binding = manifest + .binding_for(resource_type) + .ok_or(AssignmentError::PlacementAnchorMissing)?; + let anchor = *binding + .placement_anchor() + .ok_or(AssignmentError::PlacementAnchorMissing)?; + placements.insert(resource_type.clone(), anchor); + } + Ok(Self { + provider_ref, + role_ref, + scope, + supported_target_kinds: component.supported_target_kinds().clone(), + resource_types: component.exported_resource_types().clone(), + placements, + }) + } + + /// Borrow the Provider resource selected by this signed role. + pub const fn provider_ref(&self) -> &ResourceRef { + &self.provider_ref + } + + /// Borrow the controller role reference. + pub const fn role_ref(&self) -> &ResourceRef { + &self.role_ref + } + + /// Return the closed instance scope. + pub const fn scope(&self) -> ControllerInstanceScope { + self.scope + } + + /// Borrow the exclusively owned ResourceTypes. + pub const fn resource_types(&self) -> &BTreeSet { + &self.resource_types + } + + fn placement_for(&self, resource_type: &ResourceTypeName) -> Option { + self.placements.get(resource_type).copied() + } + + fn supports_target(&self, target: &AssignmentTarget) -> bool { + target + .target_kind() + .is_some_and(|kind| self.supported_target_kinds.contains(&kind)) + } +} + +impl fmt::Debug for ControllerRoleContract { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ControllerRoleContract") + .field("scope", &self.scope) + .field("resource_type_count", &self.resource_types.len()) + .field("target_kind_count", &self.supported_target_kinds.len()) + .finish() + } +} + +/// Trusted inputs for one assignment admission. +pub struct AssignmentRequest<'a> { + resource: &'a ResourceEnvelope, + role: &'a ControllerRoleContract, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + session_generation: ReconnectGeneration, + target_ready: bool, +} + +impl<'a> AssignmentRequest<'a> { + /// Bind a committed resource to a signed role and authenticated session. + pub fn new( + resource: &'a ResourceEnvelope, + role: &'a ControllerRoleContract, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + session_generation: ReconnectGeneration, + target_ready: bool, + ) -> Self { + Self { + resource, + role, + provider_generation, + controller_generation, + session_generation, + target_ready, + } + } +} + +/// A controller's non-widenable query scope. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedResourceQuery { + assignment: AssignmentIdentity, + resource_types: Vec, + resource_names: Vec, + filters: Vec, +} + +impl ScopedResourceQuery { + /// Borrow the immutable assignment evidence. + pub const fn assignment(&self) -> &AssignmentIdentity { + &self.assignment + } + + /// Borrow the exact ResourceType selector. + pub fn resource_types(&self) -> &[ResourceTypeName] { + &self.resource_types + } + + /// Borrow the exact resource-name selector. + pub fn resource_names(&self) -> &[ResourceName] { + &self.resource_names + } + + /// Borrow the assignment-bound filters. + pub fn filters(&self) -> &[ScopedResourceFilter] { + &self.filters + } + + /// Consume the query into transport-neutral parts. + pub fn into_parts( + self, + ) -> ( + AssignmentIdentity, + Vec, + Vec, + Vec, + ) { + ( + self.assignment, + self.resource_types, + self.resource_names, + self.filters, + ) + } +} + +impl fmt::Debug for ScopedResourceQuery { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedResourceQuery") + .field("resource_type_count", &self.resource_types.len()) + .field("resource_name_count", &self.resource_names.len()) + .field("filter_count", &self.filters.len()) + .finish() + } +} + +/// One exact filter minted by a controller assignment. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedResourceFilter { + field: String, + values: Vec, + assignment_bound: bool, +} + +impl ScopedResourceFilter { + /// Construct a caller-supplied narrowing filter. It cannot name the + /// assignment field; Core appends that filter itself. + pub fn narrow(field: impl Into, values: Vec) -> Result { + let field = field.into(); + if field.is_empty() + || field.len() > 64 + || !field + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')) + || values.is_empty() + || values.len() > 64 + || values.iter().any(|value| { + value.is_empty() + || value.len() > 128 + || !value + .bytes() + .all(|byte| byte.is_ascii_graphic() || byte == b' ') + }) + || field == "assignment.resourceUid" + { + return Err(AssignmentError::QueryWidened); + } + Ok(Self { + field, + values, + assignment_bound: false, + }) + } + + /// Borrow the indexed field. + pub fn field(&self) -> &str { + &self.field + } + + /// Borrow the accepted values. + pub fn values(&self) -> &[String] { + &self.values + } + + /// Whether this filter was minted by the assignment authority. + pub const fn assignment_bound(&self) -> bool { + self.assignment_bound + } +} + +impl fmt::Debug for ScopedResourceFilter { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedResourceFilter") + .field("field", &self.field) + .field("value_count", &self.values.len()) + .field("assignment_bound", &self.assignment_bound) + .finish() + } +} + +/// A single-resource mutation admitted by a controller lease. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedResourceMutation { + assignment: AssignmentIdentity, + target: ResourceRef, + verb: AssignmentVerb, +} + +impl ScopedResourceMutation { + /// Borrow the assignment evidence. + pub const fn assignment(&self) -> &AssignmentIdentity { + &self.assignment + } + + /// Borrow the exact target. + pub const fn target(&self) -> &ResourceRef { + &self.target + } + + /// Return the admitted verb. + pub const fn verb(&self) -> AssignmentVerb { + self.verb + } +} + +impl fmt::Debug for ScopedResourceMutation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedResourceMutation") + .field("target", &"") + .field("verb", &self.verb) + .finish() + } +} + +/// A non-clonable ResourceClient capability minted for one assignment. +pub struct ResourceClientLease { + identity: AssignmentIdentity, + resource_ref: ResourceRef, + resource_types: BTreeSet, + state: Arc, + allowed_verbs: BTreeSet, +} + +impl ResourceClientLease { + /// Borrow the complete immutable assignment identity. + pub const fn identity(&self) -> &AssignmentIdentity { + &self.identity + } + + /// Borrow the exact assigned resource target. + pub const fn resource_ref(&self) -> &ResourceRef { + &self.resource_ref + } + + /// Borrow the exact assigned placement target. + pub const fn target(&self) -> &AssignmentTarget { + self.identity.target() + } + + /// Return the current lease phase. + pub fn phase(&self) -> AssignmentPhase { + AssignmentPhase::from_code(self.state.phase.load(Ordering::Acquire)) + } + + /// Mint a query whose assignment filter cannot be removed or widened. + pub fn query( + &self, + resource_types: Vec, + resource_names: Vec, + filters: Vec, + ) -> Result { + let phase = self.phase(); + if !phase.admits_watch() { + return Err(match phase { + AssignmentPhase::Revoked => AssignmentError::SessionRevoked, + AssignmentPhase::Stale + | AssignmentPhase::Draining + | AssignmentPhase::Released + | AssignmentPhase::Quarantined => AssignmentError::StaleAssignment, + AssignmentPhase::Pending => AssignmentError::AssignmentMissing, + AssignmentPhase::Assigned => AssignmentError::StaleAssignment, + }); + } + if resource_types + .iter() + .any(|resource_type| !self.resource_types.contains(resource_type)) + { + return Err(AssignmentError::QueryWidened); + } + if filters + .iter() + .any(|filter| filter.field() == "assignment.resourceUid") + { + return Err(AssignmentError::QueryWidened); + } + let mut filters = filters; + filters.push(ScopedResourceFilter { + field: "assignment.resourceUid".to_owned(), + values: vec![self.identity.resource_uid().as_str().to_owned()], + assignment_bound: true, + }); + Ok(ScopedResourceQuery { + assignment: self.identity.clone(), + resource_types, + resource_names, + filters, + }) + } + + /// Admit a mutation against the resource owned by this lease. + pub fn mutation( + &self, + target: ResourceRef, + verb: AssignmentVerb, + ) -> Result { + let phase = self.phase(); + if !phase.admits_mutation() { + return Err(match phase { + AssignmentPhase::Revoked => AssignmentError::SessionRevoked, + _ => AssignmentError::StaleAssignment, + }); + } + if verb == AssignmentVerb::CommitBatch || !self.allowed_verbs.contains(&verb) { + return Err(AssignmentError::VerbNotAllowed); + } + if target != self.resource_ref { + return Err(AssignmentError::ResourceNotAssigned); + } + Ok(ScopedResourceMutation { + assignment: self.identity.clone(), + target, + verb, + }) + } + + /// Verify that a placement target remains exactly the admitted target. + pub fn target_for(&self, target: PlacementTarget) -> Result<(), AssignmentError> { + if self.target() == &AssignmentTarget::from_placement(target) { + Ok(()) + } else { + Err(AssignmentError::TargetMismatch) + } + } +} + +impl fmt::Debug for ResourceClientLease { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ResourceClientLease") + .field("resource_ref", &"") + .field("phase", &self.phase()) + .field("resource_type_count", &self.resource_types.len()) + .field("allowed_verb_count", &self.allowed_verbs.len()) + .finish() + } +} + +struct AssignmentRecord { + identity: AssignmentIdentity, + allowed_verbs: BTreeSet, + state: Arc, + children: BTreeSet, +} + +struct AssignmentLeaseState { + phase: AtomicU8, + stale_observation: AtomicBool, +} + +impl AssignmentLeaseState { + fn new(phase: AssignmentPhase) -> Self { + Self { + phase: AtomicU8::new(phase.code()), + stale_observation: AtomicBool::new(false), + } + } + + fn phase(&self) -> AssignmentPhase { + AssignmentPhase::from_code(self.phase.load(Ordering::Acquire)) + } + + fn set_phase(&self, phase: AssignmentPhase) { + self.phase.store(phase.code(), Ordering::Release); + } + + fn mark_stale(&self) { + self.stale_observation.store(true, Ordering::Release); + } +} + +/// Core's single-owner assignment registry. +#[derive(Default)] +pub struct ControllerAssignmentRegistry { + records: BTreeMap, + active_targets: BTreeMap<(ResourceTypeName, AssignmentTarget), BTreeSet>, + next_epoch: u64, +} + +impl fmt::Debug for ControllerAssignmentRegistry { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ControllerAssignmentRegistry") + .field("assignment_count", &self.records.len()) + .field("active_target_count", &self.active_targets.len()) + .finish() + } +} + +impl ControllerAssignmentRegistry { + /// Admit one resource from the committed store snapshot. + pub fn admit( + &mut self, + request: AssignmentRequest<'_>, + ) -> Result { + let resource_type = request.resource.resource_type().clone(); + if !request.role.resource_types.contains(&resource_type) { + return Err(AssignmentError::ResourceTypeUnowned); + } + if request.resource.spec().provider_ref() != Some(request.role.provider_ref()) { + return Err(AssignmentError::InvalidRole); + } + let placement_anchor = request + .role + .placement_for(&resource_type) + .ok_or(AssignmentError::PlacementAnchorMissing)?; + let target = AssignmentTarget::from_placement( + placement_anchor + .resolve(request.resource) + .map_err(|_| AssignmentError::PlacementTargetInvalid)?, + ); + if !request.role.supports_target(&target) { + return Err(AssignmentError::TargetKindUnsupported); + } + if matches!( + request.role.scope(), + ControllerInstanceScope::ZoneSingleton + if !matches!(target, AssignmentTarget::Zone(_)) + ) || matches!( + request.role.scope(), + ControllerInstanceScope::FixedExecutionTarget + if !matches!(target, AssignmentTarget::Execution { .. }) + ) { + return Err(AssignmentError::RoleContractInvalid); + } + if !request.target_ready { + return Err(AssignmentError::TargetNotReady); + } + if let Some(existing) = self.records.get(request.resource.metadata().uid()) { + if matches!( + existing.state.phase(), + AssignmentPhase::Released | AssignmentPhase::Quarantined + ) { + self.records.remove(request.resource.metadata().uid()); + } else { + return Err(AssignmentError::AssignmentConflict); + } + } + if self.records.len() >= MAX_ASSIGNMENTS { + return Err(AssignmentError::AssignmentLimit); + } + let target_key = (resource_type.clone(), target.clone()); + if self + .active_targets + .get(&target_key) + .is_some_and(|uids| !uids.is_empty()) + // A per-resource target role intentionally allows multiple + // resources at one target. The key is therefore only used for + // fixed/Zone singleton roles. + && matches!( + request.role.scope(), + ControllerInstanceScope::ZoneSingleton + | ControllerInstanceScope::FixedExecutionTarget + ) + { + return Err(AssignmentError::AssignmentConflict); + } + let epoch_value = self + .next_epoch + .checked_add(1) + .ok_or(AssignmentError::EpochExhausted)?; + self.next_epoch = epoch_value; + let epoch = AssignmentEpoch::new(epoch_value)?; + let identity = AssignmentIdentity::new( + request.resource.metadata().uid().clone(), + request.resource.metadata().revision(), + request.provider_generation, + request.controller_generation, + request.role.role_ref().clone(), + target.clone(), + request.session_generation, + epoch, + ); + let allowed_verbs = BTreeSet::from([ + AssignmentVerb::Get, + AssignmentVerb::List, + AssignmentVerb::Watch, + AssignmentVerb::Create, + AssignmentVerb::UpdateStatus, + AssignmentVerb::UpdateFinalizers, + AssignmentVerb::CommitBatch, + ]); + let state = Arc::new(AssignmentLeaseState::new(AssignmentPhase::Assigned)); + self.records.insert( + identity.resource_uid().clone(), + AssignmentRecord { + identity: identity.clone(), + allowed_verbs: allowed_verbs.clone(), + state: Arc::clone(&state), + children: BTreeSet::new(), + }, + ); + self.active_targets + .entry(target_key) + .or_default() + .insert(identity.resource_uid().clone()); + Ok(ResourceClientLease { + identity, + resource_ref: ResourceRef::new( + resource_type.clone(), + request.resource.metadata().name().clone(), + ), + resource_types: request.role.resource_types.clone(), + state, + allowed_verbs, + }) + } + + /// Rebind one live lease to the resource revision produced by its last + /// successful write without changing its assignment epoch. + pub fn rebind_revision( + &mut self, + lease: &mut ResourceClientLease, + revision: ZoneRevision, + ) -> Result<(), AssignmentError> { + let record = self + .records + .get_mut(lease.identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != lease.identity { + return Err(AssignmentError::StaleAssignment); + } + if record.state.phase() == AssignmentPhase::Revoked { + return Err(AssignmentError::SessionRevoked); + } + if record.state.phase() != AssignmentPhase::Assigned { + return Err(AssignmentError::StaleAssignment); + } + if revision < lease.identity.resource_revision() { + return Err(AssignmentError::ResourceRevisionMismatch); + } + if revision == lease.identity.resource_revision() { + return Ok(()); + } + let mut identity = lease.identity.clone(); + identity.resource_revision = revision; + record.identity = identity.clone(); + lease.identity = identity; + Ok(()) + } + + /// Return the current phase for an assignment identity. + pub fn phase(&self, identity: &AssignmentIdentity) -> Option { + self.records + .get(identity.resource_uid()) + .filter(|record| record.identity == *identity) + .map(|record| record.state.phase()) + } + + /// Mark an assignment as draining before target or generation handoff. + pub fn begin_drain(&mut self, identity: &AssignmentIdentity) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if record.state.phase() != AssignmentPhase::Assigned { + return Err(AssignmentError::AssignmentNotDraining); + } + record.state.set_phase(AssignmentPhase::Draining); + record.state.mark_stale(); + Ok(()) + } + + /// Release a drained or revoked assignment and its target index. + pub fn release(&mut self, identity: &AssignmentIdentity) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if !matches!( + record.state.phase(), + AssignmentPhase::Draining | AssignmentPhase::Revoked | AssignmentPhase::Quarantined + ) { + return Err(AssignmentError::AssignmentNotReleased); + } + if !record.children.is_empty() { + return Err(AssignmentError::ChildrenRemain); + } + record.state.set_phase(AssignmentPhase::Released); + self.remove_active_target(identity); + Ok(()) + } + + /// Quarantine an assignment whose target or child ownership is ambiguous. + pub fn quarantine(&mut self, identity: &AssignmentIdentity) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + record.state.set_phase(AssignmentPhase::Quarantined); + record.state.mark_stale(); + self.remove_active_target(identity); + Ok(()) + } + + /// Record one child resource in the assignment's narrow owner index. + pub fn record_child( + &mut self, + identity: &AssignmentIdentity, + child_uid: ResourceUid, + ) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if record.children.len() >= MAX_ASSIGNED_CHILDREN { + return Err(AssignmentError::ChildLimit); + } + record.children.insert(child_uid); + Ok(()) + } + + /// Remove one child after its terminal deletion is committed. + pub fn remove_child( + &mut self, + identity: &AssignmentIdentity, + child_uid: &ResourceUid, + ) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if !record.children.remove(child_uid) { + return Err(AssignmentError::AssignmentMissing); + } + Ok(()) + } + + /// Return the currently indexed child UIDs. + pub fn child_uids(&self, identity: &AssignmentIdentity) -> Option<&BTreeSet> { + self.records + .get(identity.resource_uid()) + .filter(|record| record.identity == *identity) + .map(|record| &record.children) + } + + /// Revoke all assignments bound to a disconnected session generation. + pub fn revoke_session(&mut self, generation: ReconnectGeneration) { + for record in self.records.values_mut() { + if record.identity.session_generation() == generation + && matches!( + record.state.phase(), + AssignmentPhase::Assigned | AssignmentPhase::Draining + ) + { + record.state.set_phase(AssignmentPhase::Revoked); + record.state.mark_stale(); + } + } + } + + /// Validate a writer against every assignment fence. + pub fn validate_writer( + &self, + identity: &AssignmentIdentity, + uid: &ResourceUid, + revision: ZoneRevision, + verb: AssignmentVerb, + ) -> Result<(), AssignmentError> { + let record = self + .records + .get(identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != *identity { + return Err(AssignmentError::StaleAssignment); + } + if record.state.phase() == AssignmentPhase::Revoked { + return Err(AssignmentError::SessionRevoked); + } + if !record.state.phase().admits_mutation() { + return Err(AssignmentError::StaleAssignment); + } + if record.identity.resource_uid() != uid { + return Err(AssignmentError::ResourceUidMismatch); + } + if record.identity.resource_revision() != revision { + return Err(AssignmentError::ResourceRevisionMismatch); + } + if !record.allowed_verbs.contains(&verb) { + return Err(AssignmentError::VerbNotAllowed); + } + Ok(()) + } + + /// Validate a read or mutation lease without a new resource snapshot. + pub fn validate_scope( + &self, + identity: &AssignmentIdentity, + verb: AssignmentVerb, + ) -> Result<(), AssignmentError> { + let record = self + .records + .get(identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != *identity { + return Err(AssignmentError::StaleAssignment); + } + if record.state.phase() == AssignmentPhase::Revoked { + return Err(AssignmentError::SessionRevoked); + } + if !record.state.phase().admits_watch() + && matches!( + verb, + AssignmentVerb::Get | AssignmentVerb::List | AssignmentVerb::Watch + ) + { + return Err(AssignmentError::StaleAssignment); + } + if !record.state.phase().admits_mutation() && verb.is_mutating() { + return Err(AssignmentError::StaleAssignment); + } + if !record.allowed_verbs.contains(&verb) { + return Err(AssignmentError::VerbNotAllowed); + } + Ok(()) + } + + /// Whether the last committed observation must be retained as stale. + pub fn observation_is_stale(&self, identity: &AssignmentIdentity) -> bool { + self.records + .get(identity.resource_uid()) + .filter(|record| record.identity == *identity) + .is_some_and(|record| record.state.stale_observation.load(Ordering::Acquire)) + } + + fn record_mut( + &mut self, + identity: &AssignmentIdentity, + ) -> Result<&mut AssignmentRecord, AssignmentError> { + let record = self + .records + .get_mut(identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != *identity { + return Err(AssignmentError::StaleAssignment); + } + Ok(record) + } + + fn remove_active_target(&mut self, identity: &AssignmentIdentity) { + self.active_targets.retain(|_, uids| { + uids.remove(identity.resource_uid()); + !uids.is_empty() + }); + } +} + +#[cfg(test)] +mod tests { + use d2b_contracts_provider::v3::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ProviderManifest, + ResourceApiBinding, RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, + UpgradeDisposition, UpgradePolicy, + }; + use d2b_contracts_resource::v3::execution_policy::BoundedToken; + use d2b_contracts_resource::v3::identity::ReconnectGeneration; + use d2b_contracts_resource::v3::{ + ControllerGeneration, PlacementTarget, ResourceEnvelope, ResourceGeneration, ResourceRef, + ResourceTypeName, ResourceUid, SchemaFingerprint, SchemaVersion, ZoneRevision, + }; + + use super::{ + AssignmentError, AssignmentPhase, AssignmentRequest, AssignmentTarget, AssignmentVerb, + ControllerAssignmentRegistry, ControllerRoleContract, ScopedCommitTransport, + ScopedResourceFilter, + }; + + const DIGEST: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + + fn digest() -> ArtifactDigest { + ArtifactDigest::parse(DIGEST).unwrap() + } + + fn fingerprint() -> SchemaFingerprint { + SchemaFingerprint::parse(DIGEST).unwrap() + } + + fn manifest() -> ProviderManifest { + let process = ResourceTypeName::parse("Process").unwrap(); + let component = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [process.clone()], + [], + [d2b_contracts_resource::v3::ExecutionDomain::System], + 8, + digest(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + fingerprint(), + SchemaVersion::new(1, 0).unwrap(), + fingerprint(), + Default::default(), + None, + None, + d2b_contracts_resource::v3::PlacementAnchor::ExecutionRef, + ) + .unwrap(); + let trust = TrustEvidence { + publisher: BoundedToken::parse("trusted").unwrap(), + root_epoch: 1, + publisher_trusted: true, + signature: SignatureState::Valid, + revocation: RevocationState::Clear, + emergency_deny: false, + provenance: PolicyEvaluation::Accepted, + sbom: PolicyEvaluation::Accepted, + license: PolicyEvaluation::Accepted, + vulnerability: PolicyEvaluation::Accepted, + conformance: PolicyEvaluation::Accepted, + support_channel: BoundedToken::parse("stable").unwrap(), + }; + ProviderManifest::new( + d2b_contracts_resource::v3::ArtifactId::parse("provider-runtime").unwrap(), + ArtifactDigestSet { + package: digest(), + executable: digest(), + manifest: digest(), + config: digest(), + schema: digest(), + service: digest(), + }, + trust, + CompatibilityRange { + api_major: 3, + api_minor: 0, + descriptor_fingerprint: fingerprint(), + state_schema_version: SchemaVersion::new(1, 0).unwrap(), + }, + [component], + [binding], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new(ControllerTargetKind::Host, digest(), digest()).unwrap(), + TargetRuntimeArtifacts::new(ControllerTargetKind::Guest, digest(), digest()).unwrap(), + ]) + .unwrap() + } + + fn process(name: &str, execution_ref: &str, revision: u64) -> ResourceEnvelope { + let uid = if name.contains("guest") { + if name.contains("second") { + "323e4567-e89b-42d3-a456-426614174002" + } else { + "223e4567-e89b-42d3-a456-426614174001" + } + } else { + "123e4567-e89b-42d3-a456-426614174000" + }; + let value = serde_json::json!({ + "apiVersion": "resources.d2bus.org/v3", + "type": "Process", + "metadata": { + "name": name, + "zone": "dev", + "uid": uid, + "generation": 1, + "revision": revision, + "ownerRef": null, + "finalizers": [], + "deletionRequestedAt": null, + "createdAt": "2026-07-22T00:00:00.000Z", + "updatedAt": "2026-07-22T00:00:00.000Z", + "managedBy": "api", + "configurationGeneration": null, + "controllerGeneration": null, + "providerGeneration": null + }, + "spec": { + "providerRef": "Provider/provider-runtime", + "executionRef": execution_ref, + "argv": ["true"] + }, + "status": { + "completedAt": null, + "conditions": [], + "lastReconciledAt": null, + "observedGeneration": 0, + "outcome": null, + "phase": "Pending", + "resource": {}, + "startedAt": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": null, + "observedGeneration": 0, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + } + } + }); + ResourceEnvelope::from_json(&serde_json::to_vec(&value).unwrap()).unwrap() + } + + fn role() -> ControllerRoleContract { + ControllerRoleContract::from_signed_manifest( + ResourceRef::parse("Provider/provider-runtime").unwrap(), + ResourceRef::parse("Process/process-controller").unwrap(), + &manifest(), + ) + .unwrap() + } + + fn request<'a>( + resource: &'a ResourceEnvelope, + role: &'a ControllerRoleContract, + provider_generation: u64, + controller_generation: u64, + session_generation: u64, + ) -> AssignmentRequest<'a> { + AssignmentRequest::new( + resource, + role, + ResourceGeneration::new(provider_generation).unwrap(), + ControllerGeneration::new(controller_generation).unwrap(), + ReconnectGeneration::new(session_generation).unwrap(), + true, + ) + } + + #[test] + fn host_and_guest_resources_have_one_disjoint_target_assignment() { + let host = process("host-process", "Host/host-system", 11); + let guest = process("guest-process", "Guest/dev-vm", 12); + let guest_second = process("guest-process-second", "Guest/dev-vm", 13); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let host_lease = registry.admit(request(&host, &role, 2, 3, 4)).unwrap(); + let guest_lease = registry.admit(request(&guest, &role, 2, 3, 5)).unwrap(); + let guest_second_lease = registry + .admit(request(&guest_second, &role, 2, 3, 6)) + .unwrap(); + + assert_ne!( + host_lease.identity().target(), + guest_lease.identity().target() + ); + assert_eq!( + guest_lease.identity().target(), + guest_second_lease.identity().target() + ); + assert_ne!( + host_lease.identity().epoch(), + guest_lease.identity().epoch() + ); + assert_eq!(host_lease.phase(), AssignmentPhase::Assigned); + assert_eq!(guest_lease.phase(), AssignmentPhase::Assigned); + assert_eq!( + host_lease.target(), + &AssignmentTarget::Execution { + kind: d2b_contracts_resource::v3::PlacementTargetKind::Host, + reference: ResourceRef::parse("Host/host-system").unwrap(), + } + ); + } + + #[test] + fn stale_assignment_epoch_rejects_status_and_finalizer_writers() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let old = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + registry.begin_drain(old.identity()).unwrap(); + registry.release(old.identity()).unwrap(); + let new = registry.admit(request(&resource, &role, 1, 1, 2)).unwrap(); + + assert_eq!( + registry.validate_writer( + old.identity(), + &resource.metadata().uid().clone(), + resource.metadata().revision(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::StaleAssignment) + ); + assert!( + registry + .validate_writer( + new.identity(), + &resource.metadata().uid().clone(), + resource.metadata().revision(), + AssignmentVerb::UpdateFinalizers, + ) + .is_ok() + ); + } + + #[test] + fn scoped_commit_transport_round_trips_assignment_and_mutations() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let target = ResourceRef::new( + resource.resource_type().clone(), + resource.metadata().name().clone(), + ); + let mutation = lease + .mutation(target, AssignmentVerb::UpdateStatus) + .unwrap(); + let transport = + ScopedCommitTransport::new(lease.identity().clone(), vec![mutation]).unwrap(); + let decoded = ScopedCommitTransport::decode(&transport.encode().unwrap()).unwrap(); + + assert_eq!(decoded.assignment(), lease.identity()); + assert_eq!(decoded.mutations(), transport.mutations()); + } + + #[test] + fn same_epoch_rebind_updates_the_active_writer_revision() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let mut lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let stale = lease.identity().clone(); + + registry + .rebind_revision(&mut lease, ZoneRevision::new(8)) + .unwrap(); + + assert_eq!(lease.identity().resource_revision(), ZoneRevision::new(8)); + assert!( + registry + .validate_writer( + lease.identity(), + resource.metadata().uid(), + ZoneRevision::new(8), + AssignmentVerb::UpdateStatus, + ) + .is_ok() + ); + assert_eq!( + registry.validate_writer( + &stale, + resource.metadata().uid(), + ZoneRevision::new(7), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::StaleAssignment) + ); + } + + #[test] + fn released_assignment_allows_successor_at_the_current_revision() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let mut old = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + registry + .rebind_revision(&mut old, ZoneRevision::new(8)) + .unwrap(); + registry.begin_drain(old.identity()).unwrap(); + registry.release(old.identity()).unwrap(); + + let current = process("process", "Guest/dev-vm", 8); + let successor = registry.admit(request(¤t, &role, 2, 2, 2)).unwrap(); + assert_eq!( + successor.identity().resource_revision(), + ZoneRevision::new(8) + ); + assert!( + registry + .validate_writer( + successor.identity(), + current.metadata().uid(), + ZoneRevision::new(8), + AssignmentVerb::UpdateFinalizers, + ) + .is_ok() + ); + } + + #[test] + fn disconnected_session_revokes_mutation_but_keeps_stale_observation() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 9)).unwrap(); + registry.revoke_session(ReconnectGeneration::new(9).unwrap()); + + assert_eq!( + registry.phase(lease.identity()), + Some(AssignmentPhase::Revoked) + ); + assert_eq!(lease.phase(), AssignmentPhase::Revoked); + assert_eq!( + lease.mutation( + ResourceRef::parse("Process/process").unwrap(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::SessionRevoked) + ); + assert_eq!( + registry.validate_writer( + lease.identity(), + resource.metadata().uid(), + resource.metadata().revision(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::SessionRevoked) + ); + assert!(registry.observation_is_stale(lease.identity())); + } + + #[test] + fn guest_lease_cannot_widen_to_host_or_foreign_resource() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let query = lease + .query( + vec![ResourceTypeName::parse("Process").unwrap()], + vec![], + vec![ + ScopedResourceFilter::narrow("metadata.name", vec!["process".to_owned()]) + .unwrap(), + ], + ) + .unwrap(); + assert!( + query + .filters() + .iter() + .any(|filter| filter.field() == "metadata.name") + ); + assert!( + query + .filters() + .iter() + .any(|filter| filter.field() == "assignment.resourceUid" + && filter.assignment_bound()) + ); + assert_eq!( + lease.query( + vec![ResourceTypeName::parse("Host").unwrap()], + vec![], + vec![], + ), + Err(AssignmentError::QueryWidened) + ); + assert_eq!( + lease.mutation( + ResourceRef::parse("Process/other").unwrap(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::ResourceNotAssigned) + ); + assert_eq!( + lease.target_for(PlacementTarget::Execution { + kind: d2b_contracts_resource::v3::PlacementTargetKind::Host, + reference: ResourceRef::parse("Host/host-system").unwrap(), + }), + Err(AssignmentError::TargetMismatch) + ); + } + + #[test] + fn target_handoff_requires_drain_and_release_before_reassignment() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let old = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + assert_eq!( + registry + .admit(request(&resource, &role, 2, 2, 2)) + .unwrap_err(), + AssignmentError::AssignmentConflict + ); + registry.begin_drain(old.identity()).unwrap(); + assert_eq!( + registry + .admit(request(&resource, &role, 2, 2, 2)) + .unwrap_err(), + AssignmentError::AssignmentConflict + ); + registry.release(old.identity()).unwrap(); + let replacement = registry.admit(request(&resource, &role, 2, 2, 2)).unwrap(); + assert_eq!(replacement.identity().provider_generation().get(), 2); + assert_eq!(replacement.identity().controller_generation().get(), 2); + } + + #[test] + fn child_index_must_drain_before_parent_release() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let child = ResourceUid::parse("423e4567-e89b-42d3-a456-426614174003").unwrap(); + registry + .record_child(lease.identity(), child.clone()) + .unwrap(); + registry.begin_drain(lease.identity()).unwrap(); + assert_eq!( + registry.release(lease.identity()), + Err(AssignmentError::ChildrenRemain) + ); + assert_eq!(registry.child_uids(lease.identity()).unwrap().len(), 1); + registry.remove_child(lease.identity(), &child).unwrap(); + registry.release(lease.identity()).unwrap(); + } + + #[test] + fn ambiguous_or_unready_targets_fail_closed_without_fallback() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + assert_eq!( + registry + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(1).unwrap(), + ControllerGeneration::new(1).unwrap(), + ReconnectGeneration::new(1).unwrap(), + false, + )) + .unwrap_err(), + AssignmentError::TargetNotReady + ); + let invalid = process("process", "User/not-a-target", 7); + assert_eq!( + registry + .admit(request(&invalid, &role, 1, 1, 1)) + .unwrap_err(), + AssignmentError::PlacementTargetInvalid + ); + } +} diff --git a/packages/d2b-core-controller/src/controllers.rs b/packages/d2b-core-controller/src/controllers.rs index 36a35b6fce..d1ff81412b 100644 --- a/packages/d2b-core-controller/src/controllers.rs +++ b/packages/d2b-core-controller/src/controllers.rs @@ -3,11 +3,7 @@ use std::collections::BTreeMap; use d2b_contracts_resource::v3::resource_status::MAX_STATUS_COLLECTION_ENTRIES; -use d2b_contracts_resource::v3::{ - ResourceCurrencySet, - ResourceRef, - UpdateState, -}; +use d2b_contracts_resource::v3::{ResourceCurrencySet, ResourceRef, UpdateState}; /// Closed fixed core handler set. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/packages/d2b-core-controller/src/coordinator.rs b/packages/d2b-core-controller/src/coordinator.rs index bcdaab3415..914b5e1389 100644 --- a/packages/d2b-core-controller/src/coordinator.rs +++ b/packages/d2b-core-controller/src/coordinator.rs @@ -7,10 +7,7 @@ use std::collections::{BTreeMap, btree_map::Entry}; -use d2b_contracts_resource::v3::{ - ResourceBundleGenerationId, - ZoneId, -}; +use d2b_contracts_resource::v3::{ResourceBundleGenerationId, ZoneId}; /// Closed failure from the per-Zone coordinator. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/packages/d2b-core-controller/src/dependencies.rs b/packages/d2b-core-controller/src/dependencies.rs index 975976dfc5..6c7fcbf328 100644 --- a/packages/d2b-core-controller/src/dependencies.rs +++ b/packages/d2b-core-controller/src/dependencies.rs @@ -2,10 +2,7 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; -use d2b_contracts_resource::v3::{ - ResourceRef, - ZoneRevision, -}; +use d2b_contracts_resource::v3::{ResourceRef, ZoneRevision}; use crate::hints::{ControllerLeaseKey, CoreTriggerReason, HintTarget}; @@ -325,10 +322,7 @@ impl std::error::Error for DependencyError {} #[cfg(test)] mod tests { - use d2b_contracts_resource::v3::{ - ResourceUid, - ZoneId, -}; + use d2b_contracts_resource::v3::{ResourceUid, ZoneId}; use super::*; @@ -356,7 +350,7 @@ mod tests { let mut index = DependencyIndex::default(); index .register( - controller("guest-controller"), + controller("component-sessionler"), guest.clone(), BTreeSet::from([network.clone()]), ) @@ -389,7 +383,7 @@ mod tests { let mut index = DependencyIndex::default(); index .register( - controller("guest-controller"), + controller("component-sessionler"), guest, BTreeSet::from([volume.clone()]), ) @@ -406,7 +400,7 @@ mod tests { assert_eq!( index .register( - controller("guest-controller"), + controller("component-sessionler"), target("work", "Guest", "app", 1), BTreeSet::from([target("personal", "Volume", "root", 2)]), ) @@ -531,7 +525,7 @@ mod tests { let mut index = DependencyIndex::default(); index .register( - controller("guest-controller"), + controller("component-sessionler"), guest.clone(), BTreeSet::from([gpu.clone()]), ) @@ -559,7 +553,7 @@ mod tests { let mut index = DependencyIndex::default(); index .register( - controller("guest-controller"), + controller("component-sessionler"), guest, BTreeSet::from([network.clone()]), ) @@ -573,7 +567,7 @@ mod tests { .unwrap(); assert_eq!( - index.withdraw_controller(&controller("guest-controller")), + index.withdraw_controller(&controller("component-sessionler")), 1 ); let triggers = index diff --git a/packages/d2b-core-controller/src/emergency_policy.rs b/packages/d2b-core-controller/src/emergency_policy.rs index fa4af7f03a..1cfeff0b33 100644 --- a/packages/d2b-core-controller/src/emergency_policy.rs +++ b/packages/d2b-core-controller/src/emergency_policy.rs @@ -6,15 +6,9 @@ use std::collections::BTreeMap; +use d2b_contracts_resource::v3::{ResourceUid, Timestamp}; use d2b_contracts_zone_session::v3::{ - EmergencyPolicySpec, - EmergencyPolicyStatusResource, - EmergencyScope, - effective_scope, -}; -use d2b_contracts_resource::v3::{ - ResourceUid, - Timestamp, + EmergencyPolicySpec, EmergencyPolicyStatusResource, EmergencyScope, effective_scope, }; use super::{ @@ -354,8 +348,8 @@ pub type EmergencyPolicyHandler = EmergencyPolicyAuthority; #[cfg(test)] mod tests { use super::*; + use d2b_contracts_resource::v3::ResourceGeneration; use d2b_contracts_zone_session::v3::EmergencyScope; -use d2b_contracts_resource::v3::ResourceGeneration; fn uid(value: &str) -> ResourceUid { ResourceUid::parse(value).unwrap() diff --git a/packages/d2b-core-controller/src/export_import.rs b/packages/d2b-core-controller/src/export_import.rs index 6d63e438ef..eed1321e45 100644 --- a/packages/d2b-core-controller/src/export_import.rs +++ b/packages/d2b-core-controller/src/export_import.rs @@ -8,23 +8,13 @@ //! implemented here. use d2b_contracts_provider::v3::{ - BindingTargetType, - Exportability, - ProjectionFactory, - ProviderContractError, - SEMANTIC_PROJECTION_PROTOCOL_VERSION, - SemanticProjectionProtocolVersion, -}; -use d2b_contracts_zone_session::v3::{ - ResourceExportSpec, - ResourceImportSpec, + BindingTargetType, Exportability, ProjectionFactory, ProviderContractError, + SEMANTIC_PROJECTION_PROTOCOL_VERSION, SemanticProjectionProtocolVersion, }; use d2b_contracts_resource::v3::{ - ResourceEnvelope, - ResourceRef, - ResourceTypeName, - SchemaFingerprint, + ResourceEnvelope, ResourceRef, ResourceTypeName, SchemaFingerprint, }; +use d2b_contracts_zone_session::v3::{ResourceExportSpec, ResourceImportSpec}; /// Why Core refused an export or import admission. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -429,22 +419,13 @@ pub fn projection_identity( #[cfg(test)] mod tests { - use d2b_contracts_provider::v3::{ - BindingTargetType, - Exportability, -}; + use d2b_contracts_provider::v3::{BindingTargetType, Exportability}; + use d2b_contracts_resource::v3::{ + ResourceName, ResourceTypeName, SchemaFingerprint, execution_policy::BoundedToken, + }; use d2b_contracts_zone_session::v3::{ - ConsumerZonePolicy, - ExportArbitration, - ResourceExportSpec, - ResourceImportSpec, -}; -use d2b_contracts_resource::v3::{ - ResourceName, - ResourceTypeName, - SchemaFingerprint, - execution_policy::BoundedToken, -}; + ConsumerZonePolicy, ExportArbitration, ResourceExportSpec, ResourceImportSpec, + }; use super::*; diff --git a/packages/d2b-core-controller/src/export_import_projection.rs b/packages/d2b-core-controller/src/export_import_projection.rs index 86c708931b..7c42eb2b8c 100644 --- a/packages/d2b-core-controller/src/export_import_projection.rs +++ b/packages/d2b-core-controller/src/export_import_projection.rs @@ -9,15 +9,9 @@ use std::collections::BTreeSet; use d2b_contracts_provider::v3::SemanticProjectionProtocolVersion; +use d2b_contracts_resource::v3::{FinalizerId, ResourceRef, ResourceTypeName, SchemaFingerprint}; use d2b_contracts_zone_session::v3::{ - RESOURCE_IMPORT_DRAIN_FINALIZER, - ResourceImportConditionType, -}; -use d2b_contracts_resource::v3::{ - FinalizerId, - ResourceRef, - ResourceTypeName, - SchemaFingerprint, + RESOURCE_IMPORT_DRAIN_FINALIZER, ResourceImportConditionType, }; use crate::export_import::{AdmittedImport, ProjectionServiceIdentity}; @@ -912,22 +906,14 @@ impl core::fmt::Debug for ProjectionController { #[cfg(test)] mod tests { use d2b_contracts_provider::v3::{ - BindingTargetType, - Exportability, - SemanticProjectionProtocolVersion, -}; + BindingTargetType, Exportability, SemanticProjectionProtocolVersion, + }; + use d2b_contracts_resource::v3::{ + ResourceName, ResourceTypeName, SchemaFingerprint, execution_policy::BoundedToken, + }; use d2b_contracts_zone_session::v3::{ - ConsumerZonePolicy, - ExportArbitration, - ResourceExportSpec, - ResourceImportSpec, -}; -use d2b_contracts_resource::v3::{ - ResourceName, - ResourceTypeName, - SchemaFingerprint, - execution_policy::BoundedToken, -}; + ConsumerZonePolicy, ExportArbitration, ResourceExportSpec, ResourceImportSpec, + }; use super::*; use crate::export_import::admit_import; diff --git a/packages/d2b-core-controller/src/hints.rs b/packages/d2b-core-controller/src/hints.rs index 28e63977ee..4ebe489019 100644 --- a/packages/d2b-core-controller/src/hints.rs +++ b/packages/d2b-core-controller/src/hints.rs @@ -3,13 +3,8 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use d2b_contracts_resource::v3::{ - ControllerGeneration, - ObservedGeneration, - ResourceGeneration, - ResourceRef, - ResourceTypeName, - ZoneId, - ZoneRevision, + ControllerGeneration, ObservedGeneration, ResourceGeneration, ResourceRef, ResourceTypeName, + ZoneId, ZoneRevision, }; pub use d2b_controller_toolkit::{ diff --git a/packages/d2b-core-controller/src/lib.rs b/packages/d2b-core-controller/src/lib.rs index fd10b74c84..80634ae4bb 100644 --- a/packages/d2b-core-controller/src/lib.rs +++ b/packages/d2b-core-controller/src/lib.rs @@ -12,9 +12,11 @@ pub mod authority; pub mod authority_persistence; pub mod authz; pub mod authz_audit; +pub mod binding_children; pub mod budgets; pub mod cleanup; pub mod configuration; +pub mod controller_assignment; pub mod controllers; pub mod coordinator; pub mod dependencies; @@ -40,6 +42,16 @@ pub mod zone_links; pub mod zone_status; pub mod zonelink; +pub use binding_children::{ + BindingChildMaterializationError, BindingChildReconciler, BindingChildResource, + materialize_child_create_payload, observed_child_from_resource, semantic_child_digest, +}; +pub use controller_assignment::{ + AssignmentEpoch, AssignmentError, AssignmentIdentity, AssignmentPhase, AssignmentRequest, + AssignmentTarget, AssignmentTransportError, AssignmentVerb, ControllerAssignmentRegistry, + ControllerRoleContract, MAX_SCOPED_COMMIT_TRANSPORT_BYTES, ResourceClientLease, + ScopedCommitTransport, ScopedResourceFilter, ScopedResourceMutation, ScopedResourceQuery, +}; pub use controllers::{ AggregateHealth, CoreHandlerKind, CoreHandlerRegistry, CurrencyAggregation, CurrencyAggregationError, HandlerOutcome, HandlerPhase, HandlerStatus, diff --git a/packages/d2b-core-controller/src/optional_state_admission.rs b/packages/d2b-core-controller/src/optional_state_admission.rs index 2f4403cec0..051fe513b7 100644 --- a/packages/d2b-core-controller/src/optional_state_admission.rs +++ b/packages/d2b-core-controller/src/optional_state_admission.rs @@ -5,11 +5,7 @@ //! or an effect. A later dispatcher must consume this result before it creates //! a declared Volume or launches the component. -use d2b_contracts_provider::v3::{ - ComponentDescriptor, - ComponentStateNamespace, - StorageNeed, -}; +use d2b_contracts_provider::v3::{ComponentDescriptor, ComponentStateNamespace, StorageNeed}; /// Whether one declared payload genuinely requires a state Volume. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -169,17 +165,11 @@ mod tests { use std::collections::BTreeMap; use d2b_contracts_provider::v3::{ - ArtifactDigest, - ComponentStateKind, - ComponentStateNamespace, - ComponentStateView, - ComponentType, - StatePlacementMode, + ArtifactDigest, ComponentStateKind, ComponentStateNamespace, ComponentStateView, + ComponentType, StatePlacementMode, }; use d2b_contracts_resource::v3::{ - ResourceTypeName, - SchemaFingerprint, - SchemaVersion, + ResourceTypeName, SchemaFingerprint, SchemaVersion, execution_policy::{BoundedToken, ExecutionDomain}, volume::ViewRight, volume_state::{MigrationPolicy, PersistenceClass, SensitivityClass, VolumeStateSchemaId}, diff --git a/packages/d2b-core-controller/src/owner_reconcile.rs b/packages/d2b-core-controller/src/owner_reconcile.rs index deb30ec665..2f34610d87 100644 --- a/packages/d2b-core-controller/src/owner_reconcile.rs +++ b/packages/d2b-core-controller/src/owner_reconcile.rs @@ -2,11 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_resource::v3::{ - ResourceRef, - ResourceUid, - ZoneRevision, -}; +use d2b_contracts_resource::v3::{ResourceRef, ResourceUid, ZoneRevision}; use crate::hints::HintTarget; @@ -93,6 +89,7 @@ pub struct ObservedChild { revision: ZoneRevision, payload_digest: String, deletion_requested: bool, + deletion_ready: bool, } impl ObservedChild { @@ -102,9 +99,24 @@ impl ObservedChild { revision: ZoneRevision, payload_digest: impl Into, deletion_requested: bool, + ) -> Result { + Self::with_deletion_state(target, revision, payload_digest, deletion_requested, false) + } + + /// Construct an observed index row with the complete deletion state. + pub fn with_deletion_state( + target: HintTarget, + revision: ZoneRevision, + payload_digest: impl Into, + deletion_requested: bool, + deletion_ready: bool, ) -> Result { let payload_digest = payload_digest.into(); - if revision.get() == 0 || payload_digest.is_empty() || payload_digest.len() > 128 { + if revision.get() == 0 + || payload_digest.is_empty() + || payload_digest.len() > 128 + || deletion_ready && !deletion_requested + { return Err(OwnerReconcileError::InvalidChild); } Ok(Self { @@ -112,6 +124,7 @@ impl ObservedChild { revision, payload_digest, deletion_requested, + deletion_ready, }) } @@ -124,6 +137,17 @@ impl ObservedChild { pub fn payload_digest(&self) -> &str { &self.payload_digest } + + /// Whether the Resource API has requested deletion for this child. + pub const fn deletion_requested(&self) -> bool { + self.deletion_requested + } + + /// Whether the child has no remaining finalizers and can be physically + /// deleted on the next exact mutation. + pub const fn deletion_ready(&self) -> bool { + self.deletion_ready + } } impl core::fmt::Debug for ObservedChild { @@ -155,6 +179,11 @@ pub enum OwnerMutation { expected_uid: ResourceUid, expected_revision: ZoneRevision, }, + Delete { + target: ResourceRef, + expected_uid: ResourceUid, + expected_revision: ZoneRevision, + }, } impl core::fmt::Debug for OwnerMutation { @@ -196,6 +225,16 @@ impl core::fmt::Debug for OwnerMutation { .field("has_expected_uid", &true) .field("expected_revision", expected_revision) .finish(), + Self::Delete { + target, + expected_revision, + .. + } => f + .debug_struct("OwnerMutation::Delete") + .field("target_type", target.resource_type()) + .field("has_expected_uid", &true) + .field("expected_revision", expected_revision) + .finish(), } } } @@ -204,6 +243,7 @@ impl core::fmt::Debug for OwnerMutation { #[derive(Debug, Clone, PartialEq, Eq)] pub struct OwnerReconcilePlan { mutations: Vec, + pending: bool, } impl OwnerReconcilePlan { @@ -214,7 +254,7 @@ impl OwnerReconcilePlan { /// Whether the complete child set is converged. pub fn is_converged(&self) -> bool { - self.mutations.is_empty() + self.mutations.is_empty() && !self.pending } } @@ -282,12 +322,19 @@ impl OwnerIndex { .cloned() .ok_or(OwnerReconcileError::OwnerNotRelisted)?; let mut mutations = Vec::new(); + let mut pending = false; for (target, desired) in &desired_by_ref { match observed.get(target) { None => mutations.push(OwnerMutation::Create { target: target.clone(), canonical_resource: desired.canonical_resource.clone(), }), + Some(actual) if actual.deletion_requested => { + // A deleting child cannot be recreated in place. Wait for + // the next authoritative relist before declaring the + // owner converged. + pending = true; + } Some(actual) if actual.payload_digest != desired.payload_digest => { mutations.push(OwnerMutation::Repair { target: target.clone(), @@ -306,9 +353,19 @@ impl OwnerIndex { expected_uid: actual.target.uid().clone(), expected_revision: actual.revision, }); + } else if !desired_by_ref.contains_key(&target) { + if actual.deletion_ready() { + mutations.push(OwnerMutation::Delete { + target, + expected_uid: actual.target.uid().clone(), + expected_revision: actual.revision, + }); + } else { + pending = true; + } } } - Ok(OwnerReconcilePlan { mutations }) + Ok(OwnerReconcilePlan { mutations, pending }) } /// Number of children in the latest complete relist. @@ -513,10 +570,7 @@ impl std::error::Error for OwnerGraphError {} #[cfg(test)] mod tests { - use d2b_contracts_resource::v3::{ - ResourceUid, - ZoneId, -}; + use d2b_contracts_resource::v3::{ResourceUid, ZoneId}; use super::*; @@ -557,6 +611,22 @@ mod tests { .unwrap() } + fn observed_deleting( + resource_type: &str, + name: &str, + suffix: u8, + revision: u64, + digest: &str, + ) -> ObservedChild { + ObservedChild::new( + target("work", resource_type, name, suffix), + ZoneRevision::new(revision), + digest, + true, + ) + .unwrap() + } + #[test] fn complete_relist_drives_create_repair_and_delete_plan() { let owner = target("work", "Guest", "desktop", 1); @@ -638,6 +708,25 @@ mod tests { assert!(plan.is_converged()); } + #[test] + fn deleting_children_keep_owner_pending_until_relisted_absent() { + let owner = target("work", "Guest", "desktop", 1); + let mut index = OwnerIndex::new(limits()); + index + .relist( + owner.clone(), + vec![observed_deleting("Endpoint", "endpoint", 2, 2, "current")], + ) + .unwrap(); + + let plan = index.plan(&owner, Vec::new()).unwrap(); + assert!(plan.mutations().is_empty()); + assert!(!plan.is_converged()); + + index.relist(owner.clone(), Vec::new()).unwrap(); + assert!(index.plan(&owner, Vec::new()).unwrap().is_converged()); + } + #[test] fn planning_fails_closed_until_the_owner_index_is_relisted() { let owner = target("work", "Guest", "desktop", 1); diff --git a/packages/d2b-core-controller/src/ownership.rs b/packages/d2b-core-controller/src/ownership.rs index 2739b04039..689c3043fc 100644 --- a/packages/d2b-core-controller/src/ownership.rs +++ b/packages/d2b-core-controller/src/ownership.rs @@ -2,10 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_resource::v3::{ - FinalizerId, - ResourceRef, -}; +use d2b_contracts_resource::v3::{FinalizerId, ResourceRef}; use d2b_controller_toolkit::ResourceKey; use crate::audit::{AuditEvent, resource_name_digest}; @@ -384,10 +381,7 @@ pub fn commit_atomic_deletion( #[cfg(test)] mod tests { - use d2b_contracts_resource::v3::{ - ResourceUid, - ZoneId, -}; + use d2b_contracts_resource::v3::{ResourceUid, ZoneId}; use super::*; @@ -531,11 +525,8 @@ mod tests { timestamp.clone(), ); assert_eq!( - commit_atomic_deletion( - blocked, - d2b_contracts_resource::v3::ZoneRevision::new(4) - ) - .unwrap_err(), + commit_atomic_deletion(blocked, d2b_contracts_resource::v3::ZoneRevision::new(4)) + .unwrap_err(), AtomicDeletionError::FinalizersRemain ); let committed = commit_atomic_deletion( diff --git a/packages/d2b-core-controller/src/providers.rs b/packages/d2b-core-controller/src/providers.rs index 76899803da..4d20a1ad83 100644 --- a/packages/d2b-core-controller/src/providers.rs +++ b/packages/d2b-core-controller/src/providers.rs @@ -1,13 +1,7 @@ //! Provider lifecycle validation and child-resource planning. -use d2b_contracts_provider::v3::{ - ComponentType, - ProviderManifest, -}; -use d2b_contracts_resource::v3::{ - ResourceRef, - SchemaFingerprint, -}; +use d2b_contracts_provider::v3::{ComponentType, ProviderManifest}; +use d2b_contracts_resource::v3::{ResourceRef, SchemaFingerprint}; /// Provider lifecycle phase derived from exact child observations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -135,6 +129,9 @@ impl ProviderHandler { required_descriptor_fingerprint, ) .map_err(|_| ProviderError::TrustOrCompatibilityDenied)?; + manifest + .validate_installation_contract() + .map_err(|_| ProviderError::GraphInvalid)?; if !observation.package_present { return Err(ProviderError::PackageUnavailable); } @@ -210,24 +207,17 @@ impl ProviderHandler { #[cfg(test)] mod tests { + use d2b_contracts_provider::v3::UpgradePolicy as ProviderUpgradePolicy; use d2b_contracts_provider::v3::{ - UpgradePolicy as ProviderUpgradePolicy, -}; - use d2b_contracts_provider::v3::{ - ArtifactDigest, - ArtifactDigestSet, - CompatibilityRange, - ComponentDescriptor, - PolicyEvaluation, - RevocationState, - SignatureState, - TrustEvidence, - UpgradeDisposition, -}; -use d2b_contracts_resource::v3::{ - ArtifactId, - execution_policy::{BoundedToken, ExecutionDomain}, -}; + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ControllerTargetKind, EffectPortClass, + PolicyEvaluation, RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, + UpgradeDisposition, + }; + use d2b_contracts_resource::v3::{ + ArtifactId, + execution_policy::{BoundedToken, ExecutionDomain}, + }; use super::*; @@ -267,8 +257,7 @@ use d2b_contracts_resource::v3::{ api_major: 3, api_minor: 0, descriptor_fingerprint: fingerprint(), - state_schema_version: d2b_contracts_resource::v3::SchemaVersion::new(1, 0) - .unwrap(), + state_schema_version: d2b_contracts_resource::v3::SchemaVersion::new(1, 0).unwrap(), }, [ComponentDescriptor::new( BoundedToken::parse("service").unwrap(), @@ -281,6 +270,24 @@ use d2b_contracts_resource::v3::{ [], false, ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("service").unwrap(), + }) + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest(), + [EffectPortClass::Runtime], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest(), + [EffectPortClass::Runtime], + ) + .unwrap(), + ]) .unwrap()], [], [], @@ -291,6 +298,11 @@ use d2b_contracts_resource::v3::{ }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new(ControllerTargetKind::Host, digest(), digest()).unwrap(), + TargetRuntimeArtifacts::new(ControllerTargetKind::Guest, digest(), digest()).unwrap(), + ]) + .unwrap() } fn observation() -> ProviderObservation { diff --git a/packages/d2b-core-controller/src/quota.rs b/packages/d2b-core-controller/src/quota.rs index 5cc38b8819..f070560f08 100644 --- a/packages/d2b-core-controller/src/quota.rs +++ b/packages/d2b-core-controller/src/quota.rs @@ -8,13 +8,8 @@ use std::collections::BTreeMap; use d2b_contracts_resource::v3::{ - QuotaEnforcementPolicy, - QuotaSpec, - QuotaStatusResource, - ResourceRef, - ResourceTypeName, - ResourceUid, - Timestamp, + QuotaEnforcementPolicy, QuotaSpec, QuotaStatusResource, ResourceRef, ResourceTypeName, + ResourceUid, Timestamp, }; use super::{ @@ -494,11 +489,8 @@ mod tests { use super::*; use d2b_contracts_resource::v3::{ - QuotaCeilings, - QuotaScope, - QuotaTypeCeiling, - ResourceGeneration, -}; + QuotaCeilings, QuotaScope, QuotaTypeCeiling, ResourceGeneration, + }; fn uid(value: &str) -> ResourceUid { ResourceUid::parse(value).unwrap() diff --git a/packages/d2b-core-controller/src/rbac.rs b/packages/d2b-core-controller/src/rbac.rs index eb3624009f..0704b64b45 100644 --- a/packages/d2b-core-controller/src/rbac.rs +++ b/packages/d2b-core-controller/src/rbac.rs @@ -5,12 +5,7 @@ use std::{ sync::{Mutex, MutexGuard}, }; -use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceRef, - ResourceUid, - ZoneRevision, -}; +use d2b_contracts_resource::v3::{ConfigurationGeneration, ResourceRef, ResourceUid, ZoneRevision}; /// Policy revisions that make one positive decision valid. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] diff --git a/packages/d2b-core-controller/src/runtime.rs b/packages/d2b-core-controller/src/runtime.rs index 9617ffce6e..0b22e32b02 100644 --- a/packages/d2b-core-controller/src/runtime.rs +++ b/packages/d2b-core-controller/src/runtime.rs @@ -546,16 +546,9 @@ mod tests { use std::collections::{BTreeMap, BTreeSet, VecDeque}; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, - ObservedGeneration, - ResourceGeneration, - ResourcePhase, - ResourceRef, - ResourceTypeName, - ResourceUid, - ZoneId, -}; + ConfigurationGeneration, ControllerGeneration, ObservedGeneration, ResourceGeneration, + ResourcePhase, ResourceRef, ResourceTypeName, ResourceUid, ZoneId, + }; use d2b_controller_toolkit::{ ControllerExecutionPolicy, ControllerIdentity, ControllerSelector, ControllerVerb, ProjectionDisposition, ResourceRegistration, ResyncPolicy, Runner, RunnerConfig, diff --git a/packages/d2b-core-controller/src/zone_links.rs b/packages/d2b-core-controller/src/zone_links.rs index abcafe3d0d..7c455f1155 100644 --- a/packages/d2b-core-controller/src/zone_links.rs +++ b/packages/d2b-core-controller/src/zone_links.rs @@ -30,10 +30,10 @@ //! the session state purely from the durable [`ZoneLinkRecord`]. Replaying an //! already-committed event after restart is a no-op that plans no effect. -use d2b_contracts_zone_session::v3::{ - zone_routing::{ZoneLinkControllerGeneration, ZoneRouteFailClosedReason, ZoneSigningKeyFingerprint}, -}; use d2b_contracts_resource::v3::ResourceUid; +use d2b_contracts_zone_session::v3::zone_routing::{ + ZoneLinkControllerGeneration, ZoneRouteFailClosedReason, ZoneSigningKeyFingerprint, +}; /// Default absolute lifetime of one allocator-issued bootstrap PSK. pub const BOOTSTRAP_PSK_TTL_MS_DEFAULT: u64 = 300_000; diff --git a/packages/d2b-core-controller/src/zone_status.rs b/packages/d2b-core-controller/src/zone_status.rs index 5e10c405e7..8726536eb2 100644 --- a/packages/d2b-core-controller/src/zone_status.rs +++ b/packages/d2b-core-controller/src/zone_status.rs @@ -1,15 +1,8 @@ //! Production Zone status projection with the mandatory system-core pair. +use d2b_contracts_resource::v3::{ResourcePhase, Timestamp}; use d2b_contracts_zone_session::v3::ZoneHandlerName; -use d2b_contracts_zone_session::v3::{ - ZoneHandlerPhase, - ZoneHandlerStatus, - ZoneStatusResource, -}; -use d2b_contracts_resource::v3::{ - ResourcePhase, - Timestamp, -}; +use d2b_contracts_zone_session::v3::{ZoneHandlerPhase, ZoneHandlerStatus, ZoneStatusResource}; fn emit_handler_status( host_phase: ZoneHandlerPhase, diff --git a/packages/d2b-core/fuzz/corpus/manifest_v04/00-valid-baseline.json b/packages/d2b-core/fuzz/corpus/manifest_v04/00-valid-baseline.json index ad581710f6..a516613a1d 100644 --- a/packages/d2b-core/fuzz/corpus/manifest_v04/00-valid-baseline.json +++ b/packages/d2b-core/fuzz/corpus/manifest_v04/00-valid-baseline.json @@ -1 +1 @@ -{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"guestControl":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"},"sys-work-net":{"apiSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net.sock","audio":false,"audioService":"d2b-sys-work-net-snd.service","audioStateFile":"/var/lib/d2b/vms/sys-work-net/state/audio-state.json","bridge":"br-work-up","env":"work","gpuSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net-gpu.sock","graphics":false,"isNetVm":true,"name":"sys-work-net","netVm":null,"observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/sys-work-net/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"guestControl":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":null,"stateDir":"/var/lib/d2b/vms/sys-work-net","staticIp":"192.0.2.2","tap":"work-u2","tpm":false,"tpmSocket":"/run/swtpm/sys-work-net/sock","usbipYubikey":false,"usbipdHostIp":null}} +{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"},"sys-work-net":{"apiSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net.sock","audio":false,"audioService":"d2b-sys-work-net-snd.service","audioStateFile":"/var/lib/d2b/vms/sys-work-net/state/audio-state.json","bridge":"br-work-up","env":"work","gpuSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net-gpu.sock","graphics":false,"isNetVm":true,"name":"sys-work-net","netVm":null,"observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/sys-work-net/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":null,"stateDir":"/var/lib/d2b/vms/sys-work-net","staticIp":"192.0.2.2","tap":"work-u2","tpm":false,"tpmSocket":"/run/swtpm/sys-work-net/sock","usbipYubikey":false,"usbipdHostIp":null}} diff --git a/packages/d2b-core/fuzz/corpus/manifest_v04/02-missing-observability.json b/packages/d2b-core/fuzz/corpus/manifest_v04/02-missing-observability.json index 22ed5eb360..4b807f8af7 100644 --- a/packages/d2b-core/fuzz/corpus/manifest_v04/02-missing-observability.json +++ b/packages/d2b-core/fuzz/corpus/manifest_v04/02-missing-observability.json @@ -1 +1 @@ -{"_manifest":{"manifestVersion":6},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"guestControl":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} +{"_manifest":{"manifestVersion":6},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} diff --git a/packages/d2b-core/fuzz/corpus/manifest_v04/03-type-confusion-graphics.json b/packages/d2b-core/fuzz/corpus/manifest_v04/03-type-confusion-graphics.json index 69f01601b6..b405cfb594 100644 --- a/packages/d2b-core/fuzz/corpus/manifest_v04/03-type-confusion-graphics.json +++ b/packages/d2b-core/fuzz/corpus/manifest_v04/03-type-confusion-graphics.json @@ -1 +1 @@ -{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":"nope","isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"guestControl":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} +{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":"nope","isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} diff --git a/packages/d2b-core/fuzz/corpus/manifest_v04/04-negative-vsock-cid.json b/packages/d2b-core/fuzz/corpus/manifest_v04/04-negative-vsock-cid.json index fee8707c93..ea4b1f6f56 100644 --- a/packages/d2b-core/fuzz/corpus/manifest_v04/04-negative-vsock-cid.json +++ b/packages/d2b-core/fuzz/corpus/manifest_v04/04-negative-vsock-cid.json @@ -1 +1 @@ -{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":-1,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"guestControl":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} +{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":-1,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} diff --git a/packages/d2b-core/fuzz/corpus/manifest_v04/06-deep-nesting.json b/packages/d2b-core/fuzz/corpus/manifest_v04/06-deep-nesting.json index cbc8f33411..31eb0b4d42 100644 --- a/packages/d2b-core/fuzz/corpus/manifest_v04/06-deep-nesting.json +++ b/packages/d2b-core/fuzz/corpus/manifest_v04/06-deep-nesting.json @@ -1 +1 @@ -{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"nested":{"a":{"b":{"c":{"d":{"e":{"f":true}}}}}},"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"guestControl":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} +{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318,"signozUrl":"http://10.40.0.10:8080","vmName":"sys-obs"},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"nested":{"a":{"b":{"c":{"d":{"e":{"f":true}}}}}},"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"capabilities":{"configSync":true,"display":true,"exec":true,"inGuestObservability":true,"keys":true,"lifecycle":true,"ssh":true,"storeSync":true,"usbHotplug":true},"kind":"nixos","provider":{"driver":"cloud-hypervisor","id":"local-cloud-hypervisor","type":"local"}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}} diff --git a/packages/d2b-core/fuzz/src/bin/core.rs b/packages/d2b-core/fuzz/src/bin/core.rs index 0caf1af6b3..bf070f6407 100644 --- a/packages/d2b-core/fuzz/src/bin/core.rs +++ b/packages/d2b-core/fuzz/src/bin/core.rs @@ -239,7 +239,7 @@ fn manifest_unknown_reserved_keys_fail_closed() { fn manifest_mismatched_vm_name_is_rejected() { let error = ManifestV04::from_slice( - br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"wrong-name","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"guestControl":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, + br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"wrong-name","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, ) .expect_err("name mismatch fails"); assert_eq!(error.kind().as_str(), "manifest-parse-error"); @@ -316,7 +316,6 @@ fn build_synthetic_resolver() -> BundleResolver { host_path: "/etc/d2b/host.json".to_owned(), processes_path: "/etc/d2b/processes.json".to_owned(), privileges_path: "/etc/d2b/privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -439,7 +438,6 @@ fn build_synthetic_resolver() -> BundleResolver { }; let processes = ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: vec![VmProcessDag { vm: "work-vm".to_owned(), workload_identity: None, @@ -529,7 +527,7 @@ fn build_synthetic_resolver() -> BundleResolver { }, }], }; - const SYNTHETIC_MANIFEST: &str = r#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"work-vm":{"apiSocket":"/var/lib/d2b/vms/work-vm/work-vm.sock","audio":false,"audioService":"d2b-work-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/work-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/work-vm/work-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"work-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/work-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"guestControl":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/work-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/work-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"},"sys-work-net":{"apiSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net.sock","audio":false,"audioService":"d2b-sys-work-net-snd.service","audioStateFile":"/var/lib/d2b/vms/sys-work-net/state/audio-state.json","bridge":"br-work-up","env":"work","gpuSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net-gpu.sock","graphics":false,"isNetVm":true,"name":"sys-work-net","netVm":null,"observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/sys-work-net/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"guestControl":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":null,"stateDir":"/var/lib/d2b/vms/sys-work-net","staticIp":"192.0.2.2","tap":"work-u2","tpm":false,"tpmSocket":"/run/swtpm/sys-work-net/sock","usbipYubikey":false,"usbipdHostIp":null}}"#; + const SYNTHETIC_MANIFEST: &str = r#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"work-vm":{"apiSocket":"/var/lib/d2b/vms/work-vm/work-vm.sock","audio":false,"audioService":"d2b-work-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/work-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/work-vm/work-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"work-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/work-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/work-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/work-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"},"sys-work-net":{"apiSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net.sock","audio":false,"audioService":"d2b-sys-work-net-snd.service","audioStateFile":"/var/lib/d2b/vms/sys-work-net/state/audio-state.json","bridge":"br-work-up","env":"work","gpuSocket":"/var/lib/d2b/vms/sys-work-net/sys-work-net-gpu.sock","graphics":false,"isNetVm":true,"name":"sys-work-net","netVm":null,"observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/sys-work-net/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":null,"stateDir":"/var/lib/d2b/vms/sys-work-net","staticIp":"192.0.2.2","tap":"work-u2","tpm":false,"tpmSocket":"/run/swtpm/sys-work-net/sock","usbipYubikey":false,"usbipdHostIp":null}}"#; let manifest = ManifestV04::from_slice(SYNTHETIC_MANIFEST.as_bytes()).expect("manifest parses"); let closures = vec![ClosureMetadata { schema_version: "v2".to_owned(), diff --git a/packages/d2b-core/src/base64_codec.rs b/packages/d2b-core/src/base64_codec.rs index ab22b5288b..86c0be0611 100644 --- a/packages/d2b-core/src/base64_codec.rs +++ b/packages/d2b-core/src/base64_codec.rs @@ -1,6 +1,6 @@ //! Minimal, dependency-free standard (RFC 4648) base64 codec with padding. //! -//! The guest-control config-read transport carries raw guest bytes as a +//! The component-session config-read transport carries raw guest bytes as a //! base64 string in the public.sock `ReadGuestConfig` response (the wire //! envelope is JSON, so raw `Vec` would be serialized as a number array //! and balloon the frame). This module keeps the framework free of an extra diff --git a/packages/d2b-core/src/bundle.rs b/packages/d2b-core/src/bundle.rs index 3a72b1d9c4..d8916a43ee 100644 --- a/packages/d2b-core/src/bundle.rs +++ b/packages/d2b-core/src/bundle.rs @@ -22,9 +22,6 @@ pub struct Bundle { pub processes_path: String, /// Private privileges.json artifact path. pub privileges_path: String, - /// Trusted store path for the one-shot cutover runner. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cutover_runner_path: Option, /// Private storage lifecycle artifact path. #[serde(default, skip_serializing_if = "Option::is_none")] pub storage_path: Option, diff --git a/packages/d2b-core/src/bundle_resolver.rs b/packages/d2b-core/src/bundle_resolver.rs index ea55b1b609..2316e9f711 100644 --- a/packages/d2b-core/src/bundle_resolver.rs +++ b/packages/d2b-core/src/bundle_resolver.rs @@ -379,7 +379,7 @@ impl ResolvedRunnerIntent { let execution_ref = node .execution_ref .clone() - .unwrap_or_else(|| format!("Guest/{vm_name}")); + .unwrap_or_else(|| default_execution_ref(vm_name, &node.role)); let valid_execution_ref = execution_ref .split_once('/') .is_some_and(|(kind, name)| matches!(kind, "Host" | "Guest") && !name.is_empty()); @@ -458,6 +458,20 @@ impl ResolvedRunnerIntent { } } +/// Resolve the legacy placement default for one trusted VM-DAG role. +/// +/// Host-side infrastructure roles execute in the singleton Host target. +/// The activation runner is the only legacy role whose default placement is +/// the owning Guest. New resource-backed Process rows must carry an explicit +/// `executionRef`. +pub fn default_execution_ref(vm_name: &str, role: &ProcessRole) -> String { + if matches!(role, ProcessRole::ActivationNixosRunner) { + format!("Guest/{vm_name}") + } else { + "Host/host-system".to_owned() + } +} + // Convenience From impls across the wire (`UserNamespaceProfile`) and // intent (`UserNamespaceSpec`) types so layer boundaries can `.into()` // instead of hand-copying fields. @@ -1371,14 +1385,35 @@ impl BundleResolver { execution_domain: ProcessExecutionDomain, user_ref: Option<&str>, template: &str, + ) -> Option<&ResolvedRunnerIntent> { + self.find_runner_intent_for_process_in_vm( + None, + execution_ref, + execution_domain, + user_ref, + template, + ) + } + + /// Find the unique trusted Process runner intent, optionally restricted + /// to one VM when a shared Host execution reference is used. + pub fn find_runner_intent_for_process_in_vm( + &self, + vm_name: Option<&str>, + execution_ref: &str, + execution_domain: ProcessExecutionDomain, + user_ref: Option<&str>, + template: &str, ) -> Option<&ResolvedRunnerIntent> { let mut matches = self.runner_intents.values().filter(|intent| { - intent.execution_ref == execution_ref + vm_name.is_none_or(|vm| intent.vm_name == vm) + && intent.execution_ref == execution_ref && intent.execution_domain == execution_domain && intent.user_ref.as_deref() == user_ref && (intent.role_id == template || intent.profile_id == template - || runner_template_name(&intent.role) == Some(template)) + || runner_template_name(&intent.role) == Some(template) + || process_template_name_matches(intent, template)) }); let first = matches.next()?; matches.next().is_none().then_some(first) @@ -2591,7 +2626,7 @@ fn render_hosts_managed_block(host: &HostJson) -> String { fn build_nm_unmanaged_intents(host: &HostJson) -> BTreeMap { let mut out = BTreeMap::new(); let mut contents = String::from("# d2b-managed begin\n"); - contents.push_str("# Generated by d2b-priv-broker; do not edit by hand.\n"); + contents.push_str("# Generated by d2b-broker; do not edit by hand.\n"); contents.push_str("[keyfile]\n"); contents.push_str("unmanaged-devices="); contents.push_str(&host.network_manager.match_criteria.join(";")); @@ -2744,8 +2779,7 @@ fn runner_role_name(role: &ProcessRole) -> Option<&'static str> { match role { ProcessRole::HostReconcile | ProcessRole::StoreVirtiofsPreflight - | ProcessRole::GuestSshReadiness - | ProcessRole::GuestControlHealth + | ProcessRole::ComponentSessionHealth | ProcessRole::SecurityKeyFrontend => None, ProcessRole::SwtpmPreStartFlush => Some("swtpm-flush"), ProcessRole::Swtpm => Some("swtpm"), @@ -2756,6 +2790,7 @@ fn runner_role_name(role: &ProcessRole) -> Option<&'static str> { ProcessRole::Audio => Some("audio"), ProcessRole::CloudHypervisorRunner => Some("cloud-hypervisor"), ProcessRole::QemuMediaRunner => Some("qemu-media"), + ProcessRole::ActivationNixosRunner => Some("activation-nixos-runner"), ProcessRole::VsockRelay => Some("vsock-relay"), ProcessRole::OtelHostBridge => Some("otel-host-bridge"), ProcessRole::Usbip => Some("usbip"), @@ -2767,6 +2802,23 @@ fn runner_template_name(role: &ProcessRole) -> Option<&'static str> { runner_role_name(role) } +fn process_template_name_matches(intent: &ResolvedRunnerIntent, template: &str) -> bool { + if intent.role != ProcessRole::WaylandProxy { + return false; + } + match template { + "wayland-proxy-worker" => intent + .execution_ref + .split_once('/') + .is_some_and(|(kind, _)| kind == "Host"), + "wayland-frontend-worker" => intent + .execution_ref + .split_once('/') + .is_some_and(|(kind, _)| kind == "Guest"), + _ => false, + } +} + fn is_placeholder_runner_spec(binary_path: &str, argv: &[String], role_name: &str) -> bool { binary_path == format!("/run/current-system/sw/bin/{role_name}") && argv.len() == 1 @@ -2789,6 +2841,10 @@ fn legacy_runner_spec(vm_name: &str, role: &ProcessRole) -> Option<(String, Vec< // processes.json. There is no Cloud Hypervisor-compatible legacy // fallback for this runtime kind. ProcessRole::QemuMediaRunner => return None, + // Activation runners are Guest-local and have no host-side legacy + // fallback. Their executable and argv must be present in the + // trusted per-Guest process DAG. + ProcessRole::ActivationNixosRunner => return None, ProcessRole::VsockRelay => ("socat", format!("d2b-otel-relay@{vm_name}")), // OtelHostBridge must always carry the closed argv from // processes.json; it has no legacy singleton fallback. @@ -2802,8 +2858,7 @@ fn legacy_runner_spec(vm_name: &str, role: &ProcessRole) -> Option<(String, Vec< ProcessRole::WaylandProxy => return None, ProcessRole::HostReconcile | ProcessRole::StoreVirtiofsPreflight - | ProcessRole::GuestSshReadiness - | ProcessRole::GuestControlHealth + | ProcessRole::ComponentSessionHealth | ProcessRole::SecurityKeyFrontend => return None, }; Some(( @@ -3443,7 +3498,6 @@ mod tests { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -3466,7 +3520,6 @@ mod tests { crate::processes::ProcessesJson { schema_version: "v2".to_owned(), vms: Vec::new(), - cutover_runner: None, }, manifest, ); @@ -3745,7 +3798,6 @@ mod tests { tpm_ownership_migration_without_running_vm_mutation: true, }, }], - cutover_runner: None, }; let manifest = ManifestV04 { @@ -3830,7 +3882,6 @@ mod tests { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, diff --git a/packages/d2b-core/src/host.rs b/packages/d2b-core/src/host.rs index 5b26180ab4..a250693d7e 100644 --- a/packages/d2b-core/src/host.rs +++ b/packages/d2b-core/src/host.rs @@ -615,7 +615,6 @@ mod tests { "capabilities": { "lifecycle": true, "storeSync": false, - "guestControl": false, "exec": false, "configSync": false, "display": true, diff --git a/packages/d2b-core/src/manifest_v04.rs b/packages/d2b-core/src/manifest_v04.rs index 1ac005ee0c..f11d975610 100644 --- a/packages/d2b-core/src/manifest_v04.rs +++ b/packages/d2b-core/src/manifest_v04.rs @@ -18,7 +18,7 @@ use crate::runtime::RuntimeMetadata; /// Bumped to `6` for the local runtime/provider contract. Per-VM manifest /// entries now carry runtime/provider metadata and provider capability bits, and /// provider-specific socket/vsock fields are nullable so qemu-media entries do -/// not fake Cloud Hypervisor, guest-control, or in-guest observability values. +/// not fake Cloud Hypervisor, component-session, or in-guest observability values. /// Bumped to `7` for per-VM lifecycle graceful-shutdown metadata. /// /// During the v6 -> v7 live-host rollout the parser accepts both versions. v6 @@ -437,7 +437,7 @@ mod tests { #[test] fn mismatched_vm_name_is_rejected() { let error = ManifestV04::from_slice( - br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"wrong-name","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"guestControl":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, + br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"wrong-name","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, ) .expect_err("name mismatch fails"); assert_eq!(error.kind().as_str(), "manifest-parse-error"); @@ -463,7 +463,7 @@ mod tests { #[test] fn v6_manifest_version_loads_with_default_lifecycle() { let manifest = ManifestV04::from_slice( - br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"guestControl":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/d2b/vms/corp-vm/tpm.sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, + br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/d2b/vms/corp-vm/tpm.sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, ) .expect("v6 manifest version parses during compatibility window"); let vm = manifest.vms.get("corp-vm").expect("corp-vm present"); @@ -500,7 +500,7 @@ mod tests { #[test] fn shell_default_name_shape_is_validated() { let error = ManifestV04::from_slice( - br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"guestControl":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"shell":{"defaultName":"bad/name","enabled":true,"maxAttached":1,"maxSessions":8},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, + br#"{"_manifest":{"manifestVersion":6},"_observability":{"enabled":false,"vmName":"sys-obs","obsVsockCid":1000,"obsVsockHostSocket":"/var/lib/d2b/vms/sys-obs/vsock.sock","signozUrl":"http://10.40.0.10:8080","signozOtlpGrpcPort":4317,"signozOtlpHttpPort":4318},"corp-vm":{"apiSocket":"/var/lib/d2b/vms/corp-vm/corp-vm.sock","audio":false,"audioService":"d2b-corp-vm-snd.service","audioStateFile":"/var/lib/d2b/vms/corp-vm/state/audio-state.json","bridge":"br-work-lan","env":"work","gpuSocket":"/var/lib/d2b/vms/corp-vm/corp-vm-gpu.sock","graphics":false,"isNetVm":false,"name":"corp-vm","netVm":"sys-work-net","observability":{"agentSocket":"/run/d2b/otlp.sock","enabled":false,"vsockCid":110,"vsockHostSocket":"/var/lib/d2b/vms/corp-vm/vsock.sock"},"runtime":{"kind":"nixos","provider":{"id":"local-cloud-hypervisor","type":"local","driver":"cloud-hypervisor"},"capabilities":{"lifecycle":true,"display":true,"usbHotplug":true,"exec":true,"configSync":true,"ssh":true,"storeSync":true,"keys":true,"inGuestObservability":true}},"shell":{"defaultName":"bad/name","enabled":true,"maxAttached":1,"maxSessions":8},"sshUser":"alice","stateDir":"/var/lib/d2b/vms/corp-vm","staticIp":"10.20.0.10","tap":"work-l10","tpm":false,"tpmSocket":"/run/swtpm/corp-vm/sock","usbipYubikey":false,"usbipdHostIp":"192.0.2.1"}}"#, ) .expect_err("invalid shell defaultName must fail closed"); assert_eq!(error.kind().as_str(), "manifest-parse-error"); diff --git a/packages/d2b-core/src/privileges.rs b/packages/d2b-core/src/privileges.rs index 101dfdf1ea..b907c5936f 100644 --- a/packages/d2b-core/src/privileges.rs +++ b/packages/d2b-core/src/privileges.rs @@ -340,16 +340,6 @@ pub const PUBLIC_OPERATION_AUTHZ: &[OperationAuthzRow] = &[ BrokerRequirement::NoMutation, AuditMode::Yes, ), - row( - "host cutover", - "host cutover", - "global", - &["d2b-admin"], - true, - SecretAccess::MetadataOnly, - BrokerRequirement::Yes, - AuditMode::Yes, - ), row( "host prepare", "host", @@ -436,16 +426,6 @@ pub const PUBLIC_OPERATION_AUTHZ: &[OperationAuthzRow] = &[ BrokerRequirement::Yes, AuditMode::Yes, ), - row( - "host migrate-storage --apply", - "host/storage", - "global", - &["d2b-admin"], - true, - SecretAccess::PossiblePathsOnly, - BrokerRequirement::Yes, - AuditMode::Yes, - ), row( "up", "VM/env", @@ -870,36 +850,6 @@ pub const BROKER_OPERATION_AUTHZ: &[OperationAuthzRow] = &[ BrokerRequirement::Yes, AuditMode::Yes, ), - row( - "LaunchCutoverRunner", - "cutover-runner", - "global", - &["d2bd"], - true, - SecretAccess::MetadataOnly, - BrokerRequirement::Yes, - AuditMode::Yes, - ), - row( - "CutoverAudit", - "cutover-audit", - "global", - &["d2b-cutover-runner"], - false, - SecretAccess::MetadataOnly, - BrokerRequirement::Yes, - AuditMode::Yes, - ), - row( - "CutoverEffect", - "cutover-effect", - "global", - &["d2b-cutover-runner"], - true, - SecretAccess::MetadataOnly, - BrokerRequirement::Yes, - AuditMode::Yes, - ), row( "RunGc", "VM/global", @@ -1342,16 +1292,6 @@ pub const BROKER_OPERATION_AUTHZ: &[OperationAuthzRow] = &[ BrokerRequirement::Yes, AuditMode::Yes, ), - row( - "GuestControlSign", - "guest-control token", - "per-VM", - &["d2bd"], - false, - SecretAccess::RedactedOnly, - BrokerRequirement::Yes, - AuditMode::Yes, - ), row( "InjectSecretById", "secret/key", diff --git a/packages/d2b-core/src/processes.rs b/packages/d2b-core/src/processes.rs index 187237c43f..01d8994425 100644 --- a/packages/d2b-core/src/processes.rs +++ b/packages/d2b-core/src/processes.rs @@ -12,23 +12,6 @@ pub struct ProcessesJson { pub schema_version: String, /// Per-VM process DAGs. pub vms: Vec, - /// Optional out-of-band host cutover runner contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cutover_runner: Option, -} - -/// Private process contract for the one-shot cutover runner. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverRunnerProcess { - /// Trusted store path installed by the host-tool emitter. - pub binary_path: String, - /// The runner is never a declared persistent unit. - pub persistent: bool, - /// Closed placement label proving it is outside the VM slice. - pub cgroup_placement: String, - /// The broker transfers exactly one bootstrap descriptor. - pub single_bootstrap_fd: bool, } /// Process DAG for one VM. @@ -72,7 +55,8 @@ pub struct ProcessNode { pub id: NodeId, /// Canonical Host or Guest execution target for this trusted runner. /// - /// Omitted by legacy bundles, which resolve to `Guest/`. + /// Omitted by legacy bundles, which resolve to the singleton Host target + /// except for the Guest-local activation runner. #[serde(default, skip_serializing_if = "Option::is_none")] pub execution_ref: Option, /// Canonical execution domain for this trusted runner. @@ -255,23 +239,23 @@ pub enum ProcessRole { CloudHypervisorRunner, /// QEMU media runner. QemuMediaRunner, + /// Target-local one-shot NixOS activation runner. + /// + /// This role is emitted only for the Guest execution target. The + /// activation Provider creates an EphemeralProcess resource and the + /// target-local process Provider resolves this role from the trusted + /// bundle. + ActivationNixosRunner, /// vsock relay sidecar. VsockRelay, /// Host-to-observability-VM OTLP bridge. OtelHostBridge, - /// Guest SSH readiness probe. - /// - /// Retained for the SSH-compatibility window (old-generation VMs that - /// predate guestd). New generations use [`ProcessRole::GuestControlHealth`] - /// for framework readiness instead. - GuestSshReadiness, - /// Authenticated guest-control Health readiness probe. + /// Authenticated ComponentSession Health readiness probe. /// - /// Replaces [`ProcessRole::GuestSshReadiness`] as the framework readiness - /// gate on guest-control-capable VMs: readiness is a full authenticated - /// Hello + token challenge-response + Health over the guest-control vsock, - /// not a raw TCP-22 probe. It fails CLOSED. - GuestControlHealth, + /// Readiness is a full authenticated ComponentSession identity exchange + /// and Health check over the enrolled vsock, not a raw TCP-22 probe. + /// It fails closed. + ComponentSessionHealth, /// USBIP proxy or attach helper. Usbip, /// Guest-side CTAPHID relay frontend via UHID virtual HID device and @@ -367,16 +351,16 @@ pub enum ReadinessPredicate { Command(Vec), /// Component-specific predicate named by the emitter. ComponentSpecific(String), - /// Authenticated guest-control Health probe. Readiness requires a - /// full Hello + token challenge-response + Health over the guest-control - /// vsock - the host-side probe. Unlike [`Self::ComponentSpecific`] + /// Authenticated component-session Health probe. Readiness requires a + /// full identity-bound handshake + Health over the ComponentSession vsock - + /// the host-side probe. Unlike [`Self::ComponentSpecific`] /// (which reports ready unconditionally and would fail OPEN), this predicate /// fails CLOSED: it is ready only when the daemon completes the /// authenticated probe and the guest reports a healthy/degraded state, and /// never ready for an old-generation / unreachable / auth-failed / timed-out /// guest. The daemon resolves the per-VM vsock socket, peer credentials, and - /// broker-backed signer from its own trusted state. - GuestControlHealth { vm: String }, + /// enrolled identity evidence from its own trusted state. + ComponentSessionHealth { vm: String }, } /// v0.4.0 invariants preserved in the process contract. diff --git a/packages/d2b-core/src/provider_capabilities.rs b/packages/d2b-core/src/provider_capabilities.rs index 8a2c6c49be..746593fb4e 100644 --- a/packages/d2b-core/src/provider_capabilities.rs +++ b/packages/d2b-core/src/provider_capabilities.rs @@ -13,8 +13,8 @@ use serde::{Deserialize, Serialize}; /// Host-side audio enforcement mechanism for a provider. /// -/// Separate from [`AudioGuestEnforcementKind`] so that host and guest -/// capabilities can evolve independently, and so "guestd enforced" is +/// Separate from [`AudioGuestEnforcementKind`] so that host and target-local +/// capabilities can evolve independently, and so target-side enforcement is /// never a valid description of host-side audio behaviour. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] @@ -32,19 +32,16 @@ pub enum AudioHostEnforcementKind { /// Guest-side audio enforcement mechanism for a provider. /// -/// Separate from [`AudioHostEnforcementKind`] so that guest enforcement via -/// guestd cannot be conflated with host-side PipeWire or qemu enforcement. +/// Separate from [`AudioHostEnforcementKind`] so that target-local Process +/// enforcement cannot be conflated with host-side PipeWire or qemu enforcement. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum AudioGuestEnforcementKind { - /// Guest-side enforcement is not supported (qemu-media VMs, local-only - /// providers, or provider sandboxes without a running guestd agent). + /// Guest-side enforcement is not supported. #[default] Unsupported, - /// guestd-capable audio policy via the authenticated guest-control - /// transport (Cloud Hypervisor NixOS VMs) or provider peer transport - /// (ACA sandboxes with a guestd-compatible agent). - GuestdCapable, + /// Guest-side enforcement is provided by a signed target-local Process. + ProcessCapable, } /// Per-provider audio capability row, used by the daemon to select the @@ -62,11 +59,11 @@ pub struct AudioProviderCapability { impl AudioProviderCapability { /// Capability row for Cloud Hypervisor NixOS VMs: PipeWire vhost-user-sound - /// host enforcement plus guestd guest enforcement. + /// host enforcement plus a signed target-local audio Process. pub fn cloud_hypervisor_nixos() -> Self { Self { host_enforcement: AudioHostEnforcementKind::PipeWireVhostUserSound, - guest_enforcement: AudioGuestEnforcementKind::GuestdCapable, + guest_enforcement: AudioGuestEnforcementKind::ProcessCapable, needs_local_state_file: true, } } @@ -82,12 +79,11 @@ impl AudioProviderCapability { } /// Capability row for ACA sandbox targets: no local host enforcement; - /// guest enforcement via the guestd-compatible sandbox agent over the - /// provider peer transport. + /// guest enforcement via the signed target-local audio Process. pub fn aca_sandbox() -> Self { Self { host_enforcement: AudioHostEnforcementKind::None, - guest_enforcement: AudioGuestEnforcementKind::GuestdCapable, + guest_enforcement: AudioGuestEnforcementKind::ProcessCapable, needs_local_state_file: false, } } @@ -105,7 +101,7 @@ pub enum ConsoleBackendKind { /// Local hypervisor console backend (Cloud Hypervisor serial socket or /// broker-owned fd). LocalHypervisor, - /// Provider relay transport (ACA sandbox via ADR 0032 guestd route). + /// Provider relay transport (ACA sandbox via the provider relay route). ProviderRelay, } diff --git a/packages/d2b-core/src/realm_controller_config.rs b/packages/d2b-core/src/realm_controller_config.rs index 7b2ed828d6..eb964d466d 100644 --- a/packages/d2b-core/src/realm_controller_config.rs +++ b/packages/d2b-core/src/realm_controller_config.rs @@ -354,7 +354,6 @@ pub struct RealmControllerRuntimeCapabilities { pub lifecycle: bool, pub display: bool, pub usb_hotplug: bool, - pub guest_control: bool, pub exec: bool, pub config_sync: bool, pub ssh: bool, @@ -403,7 +402,6 @@ pub struct RealmControllerDisplayOperationCapabilities { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct RealmControllerGuestOperationCapabilities { - pub guest_control: bool, pub exec: bool, pub shell: bool, pub config_sync: bool, @@ -450,7 +448,7 @@ pub struct RealmControllerLocalWorkloadPaths { pub state_dir: PathTemplate, pub run_dir: PathTemplate, pub store_view: PathTemplate, - pub guest_control_dir: PathTemplate, + pub component_session_dir: PathTemplate, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] diff --git a/packages/d2b-core/src/runtime.rs b/packages/d2b-core/src/runtime.rs index a64fe5b610..ff7ffa0f1a 100644 --- a/packages/d2b-core/src/runtime.rs +++ b/packages/d2b-core/src/runtime.rs @@ -29,7 +29,6 @@ impl RuntimeMetadata { config_sync: true, display: true, exec: true, - guest_control: true, in_guest_observability: true, keys: true, lifecycle: true, @@ -66,8 +65,8 @@ impl RuntimeMetadata { false, ), RuntimeServiceSummary::from_process_role( - "guest-control-health", - ProcessRole::GuestControlHealth, + "component-session-health", + ProcessRole::ComponentSessionHealth, false, ), RuntimeServiceSummary::from_process_role("swtpm", ProcessRole::Swtpm, true), @@ -86,7 +85,6 @@ impl RuntimeMetadata { config_sync: false, display: true, exec: false, - guest_control: false, in_guest_observability: false, keys: false, lifecycle: true, @@ -160,7 +158,6 @@ pub struct RuntimeCapabilities { pub config_sync: bool, pub display: bool, pub exec: bool, - pub guest_control: bool, pub in_guest_observability: bool, pub keys: bool, pub lifecycle: bool, @@ -196,7 +193,6 @@ impl RuntimeOperationCapabilities { guest: RuntimeGuestCapabilities { config_sync: true, exec: true, - guest_control: true, in_guest_observability: true, keys: true, shell: true, @@ -284,7 +280,6 @@ pub struct RuntimeDisplayCapabilities { pub struct RuntimeGuestCapabilities { pub config_sync: bool, pub exec: bool, - pub guest_control: bool, pub in_guest_observability: bool, pub keys: bool, #[serde(default)] @@ -330,7 +325,7 @@ pub enum RuntimeServiceRole { Audio, Video, Network, - GuestControl, + ComponentSession, Usb, Observability, } @@ -347,8 +342,9 @@ impl From<&ProcessRole> for RuntimeServiceRole { } ProcessRole::Audio => Self::Audio, ProcessRole::CloudHypervisorRunner | ProcessRole::QemuMediaRunner => Self::Hypervisor, + ProcessRole::ActivationNixosRunner => Self::Host, ProcessRole::VsockRelay => Self::Network, - ProcessRole::GuestSshReadiness | ProcessRole::GuestControlHealth => Self::GuestControl, + ProcessRole::ComponentSessionHealth => Self::ComponentSession, ProcessRole::Usbip | ProcessRole::SecurityKeyFrontend => Self::Usb, ProcessRole::OtelHostBridge => Self::Observability, } diff --git a/packages/d2b-core/src/static_invariants.rs b/packages/d2b-core/src/static_invariants.rs index 35104d56b5..d4f38ba680 100644 --- a/packages/d2b-core/src/static_invariants.rs +++ b/packages/d2b-core/src/static_invariants.rs @@ -95,7 +95,6 @@ pub const PUBLIC_MANIFEST_FIELDS: &[&str] = &[ "displayWindow", "exec", "guest", - "guestControl", "gracefulShutdown", "hostPrepare", "inGuestObservability", diff --git a/packages/d2b-core/src/storage.rs b/packages/d2b-core/src/storage.rs index dfc7ff4a4e..3525337cd4 100644 --- a/packages/d2b-core/src/storage.rs +++ b/packages/d2b-core/src/storage.rs @@ -168,7 +168,6 @@ pub enum CleanupPolicy { Boot, ProcessExitWithProof, VmStopWithProof, - CutoverOnly, External, } diff --git a/packages/d2b-core/src/sync.rs b/packages/d2b-core/src/sync.rs index be6cd1aa65..d5b20d08f0 100644 --- a/packages/d2b-core/src/sync.rs +++ b/packages/d2b-core/src/sync.rs @@ -120,7 +120,6 @@ pub enum LockStaleKind { PidfdProofRequired, CgroupEmptyProofRequired, FileRecordOwnerMatch, - CutoverOnly, ManualRecovery, } diff --git a/packages/d2b-core/tests/broker_network_prep.rs b/packages/d2b-core/tests/broker_network_prep.rs index 4a4bf924eb..88c342652b 100644 --- a/packages/d2b-core/tests/broker_network_prep.rs +++ b/packages/d2b-core/tests/broker_network_prep.rs @@ -26,7 +26,6 @@ fn trusted_bundle_resolves_network_operation_rows_without_wire_paths() { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -48,7 +47,6 @@ fn trusted_bundle_resolves_network_operation_rows_without_wire_paths() { host, ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: Vec::new(), }, ManifestV04::from_slice(MANIFEST_JSON).expect("manifest fixture parses"), @@ -101,7 +99,6 @@ fn resolver_with_bundle_hash(bundle_hash: Option) -> BundleResolver { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -123,7 +120,6 @@ fn resolver_with_bundle_hash(bundle_hash: Option) -> BundleResolver { serde_json::from_str(HOST_JSON).expect("host fixture parses"), ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: Vec::new(), }, ManifestV04::from_slice(MANIFEST_JSON).expect("manifest fixture parses"), diff --git a/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs b/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs index e7fa631140..4d148dbd00 100644 --- a/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs +++ b/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs @@ -81,7 +81,6 @@ fn bundle() -> Bundle { host_path: "host.json".to_owned(), processes_path: "processes.json".to_owned(), privileges_path: "privileges.json".to_owned(), - cutover_runner_path: None, storage_path: None, sync_path: None, allocator_path: None, @@ -256,7 +255,6 @@ fn bundle_resolver_runner_intents_match_typed_process_node_helper_for_runner_rol let nodes = runner_nodes(); let processes = ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: vec![VmProcessDag { workload_identity: None, vm: VM.to_owned(), @@ -292,7 +290,6 @@ fn generic_process_template_lookup_does_not_require_resource_name_to_match_role_ let nodes = runner_nodes(); let processes = ProcessesJson { schema_version: "v2".to_owned(), - cutover_runner: None, vms: vec![VmProcessDag { workload_identity: None, vm: VM.to_owned(), @@ -311,7 +308,7 @@ fn generic_process_template_lookup_does_not_require_resource_name_to_match_role_ let resource_name = "audio-resource"; let intent = resolver .find_runner_intent_for_process( - "Guest/test-vm", + "Host/host-system", d2b_core::processes::ProcessExecutionDomain::System, None, "audio", diff --git a/packages/d2b-core/tests/bundle_resolver_runner_intents.rs b/packages/d2b-core/tests/bundle_resolver_runner_intents.rs index 3c2ad2cd5c..c13370fbad 100644 --- a/packages/d2b-core/tests/bundle_resolver_runner_intents.rs +++ b/packages/d2b-core/tests/bundle_resolver_runner_intents.rs @@ -250,7 +250,7 @@ fn bundle_resolver_loads_runner_intents_for_workload_vm() { ); // The cloud-hypervisor and virtiofsd nodes are both runner-shaped - // (vs HostReconcile/StoreVirtiofsPreflight/GuestSshReadiness which + // (vs HostReconcile/StoreVirtiofsPreflight/ComponentSessionHealth which // are readiness-only and skipped by build_runner_intents). let ch_present = intent_ids.iter().any(|id| id.contains("cloud-hypervisor")); let vfsd_present = intent_ids.iter().any(|id| id.contains("virtiofsd")); diff --git a/packages/d2b-core/tests/realm_controller_config.rs b/packages/d2b-core/tests/realm_controller_config.rs index 7aa2fe0755..b914c04102 100644 --- a/packages/d2b-core/tests/realm_controller_config.rs +++ b/packages/d2b-core/tests/realm_controller_config.rs @@ -68,7 +68,6 @@ fn realm_controllers_fixture() -> serde_json::Value { "lifecycle": true, "display": true, "usbHotplug": true, - "guestControl": true, "exec": true, "configSync": true, "ssh": true, @@ -96,7 +95,6 @@ fn realm_controllers_fixture() -> serde_json::Value { "waylandProxy": true }, "guest": { - "guestControl": true, "exec": true, "shell": true, "configSync": true, @@ -133,7 +131,6 @@ fn realm_controllers_fixture() -> serde_json::Value { "lifecycle": true, "display": true, "usbHotplug": true, - "guestControl": true, "exec": true, "configSync": true, "ssh": true, @@ -161,7 +158,6 @@ fn realm_controllers_fixture() -> serde_json::Value { "waylandProxy": true }, "guest": { - "guestControl": true, "exec": true, "shell": true, "configSync": true, @@ -185,7 +181,7 @@ fn realm_controllers_fixture() -> serde_json::Value { "stateDir": "/var/lib/d2b/vms/corp-vm", "runDir": "/run/d2b/vms/corp-vm", "storeView": "/var/lib/d2b/vms/corp-vm/store-view", - "guestControlDir": "/run/d2b/vms/corp-vm/guest-control" + "componentSessionDir": "/run/d2b/vms/corp-vm/component-session" } } ], diff --git a/packages/d2b-core/tests/runtime_dtos.rs b/packages/d2b-core/tests/runtime_dtos.rs index 8e5125e497..63aa70693a 100644 --- a/packages/d2b-core/tests/runtime_dtos.rs +++ b/packages/d2b-core/tests/runtime_dtos.rs @@ -12,7 +12,6 @@ fn local_nixos_advertises_positive_operation_axes() { let runtime = RuntimeMetadata::local_nixos(); assert!(runtime.capabilities.lifecycle); - assert!(runtime.capabilities.guest_control); assert!(runtime.operation_capabilities.lifecycle.start); assert!(runtime.operation_capabilities.lifecycle.switch); assert!(runtime.operation_capabilities.display.graphics); @@ -40,10 +39,7 @@ fn local_nixos_advertises_positive_operation_axes() { assert!(runtime.services.iter().any(|service| service.optional)); let value = serde_json::to_value(runtime).expect("serializes"); - assert_eq!( - value.pointer("/operationCapabilities/guest/guestControl"), - Some(&json!(true)) - ); + assert!(value.pointer("/operationCapabilities/guest/componentSession").is_none()); assert!(value.pointer("/services/3/processRole").is_none()); assert_eq!( value.pointer("/autostartPolicy"), @@ -52,11 +48,10 @@ fn local_nixos_advertises_positive_operation_axes() { } #[test] -fn local_qemu_media_uses_same_axes_without_guest_control() { +fn local_qemu_media_uses_same_axes_without_guest_operations() { let runtime = RuntimeMetadata::local_qemu_media(); assert!(runtime.capabilities.lifecycle); - assert!(!runtime.capabilities.guest_control); assert!(runtime.operation_capabilities.lifecycle.start); assert!(!runtime.operation_capabilities.lifecycle.switch); assert!(runtime.operation_capabilities.media.qemu_media); @@ -79,7 +74,6 @@ fn legacy_runtime_metadata_defaults_new_fields() { "configSync": true, "display": true, "exec": true, - "guestControl": true, "inGuestObservability": true, "keys": true, "lifecycle": true, diff --git a/packages/d2b-cutover/BUILD.bazel b/packages/d2b-cutover/BUILD.bazel deleted file mode 100644 index 6aa54fd25b..0000000000 --- a/packages/d2b-cutover/BUILD.bazel +++ /dev/null @@ -1,113 +0,0 @@ -load("@crates//:defs.bzl", "all_crate_deps") -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") - -exports_files( - ["BUILD.bazel"], - visibility = ["//visibility:public"], -) - -rust_library( - name = "d2b_cutover", - srcs = glob(["src/**/*.rs"], allow_empty = True), - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - "//packages/d2b-contracts:d2b_contracts", - "//packages/d2b-contracts-broker:d2b_contracts_broker", - "//packages/d2b-contracts-resource:d2b_contracts_resource", - ] + all_crate_deps(normal = True, cargo_only = True), -) - -rust_binary( - name = "d2b-cutover-runner", - srcs = glob(["src/**/*.rs"], allow_empty = True), - crate_root = "src/bin/d2b-cutover-runner.rs", - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - ":d2b_cutover", - "//packages/d2b-contracts:d2b_contracts", - "//packages/d2b-contracts-broker:d2b_contracts_broker", - "//packages/d2b-contracts-resource:d2b_contracts_resource", - ] + all_crate_deps(normal = True, cargo_only = True), -) - -rust_test( - name = "crash_recovery", - srcs = ["tests/crash_recovery.rs"], - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - ":d2b_cutover", - ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - -rust_test( - name = "reset_scope", - srcs = ["tests/reset_scope.rs"], - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - ":d2b_cutover", - ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - -rust_test( - name = "runner_contract", - srcs = ["tests/runner_contract.rs"], - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - ":d2b_cutover", - ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - -rust_test( - name = "state_machine", - srcs = ["tests/state_machine.rs"], - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - ":d2b_cutover", - ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - -rust_test( - name = "d2b_cutover_test", - compile_data = ["Cargo.toml"], - crate = ":d2b_cutover", - deps = [] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - -rust_test( - name = "d2b-cutover-runner_test", - compile_data = ["Cargo.toml"], - crate = ":d2b-cutover-runner", - data = [":d2b-cutover-runner"], - env = {"CARGO_BIN_EXE_d2b-cutover-runner": "$(rootpath :d2b-cutover-runner)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-cutover-runner": "$(rootpath :d2b-cutover-runner)"}, - deps = [] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - -# keep -filegroup( - name = "cargo_workspace_sources", - srcs = glob( - [ - "Cargo.lock", - "Cargo.toml", - "benches/**/*.rs", - "build.rs", - "examples/**/*.rs", - "src/**/*.rs", - "tests/**/*.rs", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) - -test_suite( - name = "all-tests", - tags = ["-local", "-no-remote-exec"], -) diff --git a/packages/d2b-cutover/Cargo.toml b/packages/d2b-cutover/Cargo.toml deleted file mode 100644 index 0e81af3e3c..0000000000 --- a/packages/d2b-cutover/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "d2b-cutover" -version = "0.0.0-bootstrap" -edition = "2024" -publish = false -license.workspace = true - -[lints] -workspace = true - -[dependencies] -d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } -d2b-contracts-broker = { path = "../d2b-contracts-broker", version = "0.0.0-bootstrap" } -d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } -nix = { version = "0.29", features = ["fs", "process", "socket", "user"] } -serde.workspace = true -serde_json.workspace = true - -[[bin]] -name = "d2b-cutover-runner" -path = "src/bin/d2b-cutover-runner.rs" diff --git a/packages/d2b-cutover/src/bin/d2b-cutover-runner.rs b/packages/d2b-cutover/src/bin/d2b-cutover-runner.rs deleted file mode 100644 index 53403b1bd1..0000000000 --- a/packages/d2b-cutover/src/bin/d2b-cutover-runner.rs +++ /dev/null @@ -1,3002 +0,0 @@ -//! One-shot, out-of-band cutover owner. -//! -//! The broker launches this process before control-plane drain and transfers -//! the bootstrap over one close-on-exec-controlled fd. The runner owns the -//! durable journal, OFD lock, and drain-window socket. It never accepts a -//! raw host path or performs a host mutation directly. - -use std::{ - collections::BTreeSet, - env, - fs::OpenOptions, - io::Read as _, - os::{fd::AsRawFd, unix::fs::OpenOptionsExt}, - path::PathBuf, - time::{SystemTime, UNIX_EPOCH}, -}; - -use d2b_contracts::{MAX_FRAME_SIZE, types::BundleOpId}; -use d2b_contracts_broker::broker_wire::{ - BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, CanonicalAuditDigest, - CutoverAuditRequest, CutoverAuditTransition, CutoverEffectAuthority, CutoverEffectKind, - CutoverEffectOutcome, CutoverEffectPayload, CutoverEffectRequest, CutoverReplayClass, -}; -use d2b_cutover::{ - ApplyContext, ArtifactId, AuditEvidence, CapabilityLedger, CompletionEvidence, CutoverPhase, - EffectEvidence, EffectKind, EffectOutcome, EffectRequest, FailureCode, HoldReason, - HostLockContract, Journal, JournalBinding, MAX_RUNNER_FRAME_BYTES, Operation, - OperationInventory, OperationKind, RUNNER_BOOTSTRAP_FD, RUNNER_SOCKET_TIMEOUT, - ReadOnlyEvidence, ReplayClass, RunnerBootstrap, RunnerCommand, RunnerPaths, RunnerPeer, - RunnerResponse, RunnerSocket, RunnerStatus, RunnerVerificationInput, RunnerZoneVerification, - StepId, acquire_operation_lock, persist_journal, write_response, -}; -use nix::{ - fcntl::{FcntlArg, FdFlag, fcntl}, - libc, - sys::socket::{ - AddressFamily, MsgFlags, SockFlag, SockType, UnixAddr, connect, recv, send, setsockopt, - socket, sockopt, - }, - sys::time::{TimeVal, TimeValLike}, - unistd::setsid, -}; - -fn main() -> std::process::ExitCode { - match run() { - Ok(()) => std::process::ExitCode::SUCCESS, - Err(error) => { - eprintln!("d2b-cutover-runner: {error}"); - std::process::ExitCode::from(78) - } - } -} - -fn run() -> Result<(), RunnerError> { - if nix::unistd::geteuid().as_raw() != 0 { - return Err(RunnerError::NotRoot); - } - let bootstrap_fd_value = env::var("D2B_CUTOVER_BOOTSTRAP_FD").ok(); - let bootstrap_fd = bootstrap_fd_from_env(bootstrap_fd_value.as_deref())?; - let bootstrap_bytes = read_bootstrap_fd(bootstrap_fd)?; - setsid().map_err(|_| RunnerError::Session)?; - - let state_root = env::var_os("D2B_CUTOVER_STATE_DIR") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/var/lib/d2b")); - let socket_root = env::var_os("D2B_CUTOVER_SOCKET_DIR") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/run/d2b")); - let mut ledger = CapabilityLedger::default(); - let (bootstrap, consumed) = - RunnerBootstrap::decode_and_consume(&bootstrap_bytes, now_ms(), &mut ledger) - .map_err(|_| RunnerError::Bootstrap)?; - let paths = RunnerPaths::new_with_socket_root(state_root, socket_root, consumed.operation_id()); - let lock = acquire_operation_lock(&paths).map_err(|_| RunnerError::Lock)?; - let (bootstrap, mut operation, adopted) = match load_existing_operation(&paths, &bootstrap)? { - Some((_persisted_bootstrap, operation)) => (bootstrap.clone(), operation, true), - None => ( - bootstrap.clone(), - Operation::new(bootstrap.request.clone(), &bootstrap.preview) - .map_err(|_| RunnerError::Request)?, - false, - ), - }; - let mut host_lock = HostLockContract::new(); - if adopted { - operation - .acquire_host_lock(&mut host_lock) - .map_err(|_| RunnerError::Request)?; - } - if !adopted { - persist_journal( - &paths.journal, - &bootstrap, - &operation - .journal_bytes() - .map_err(|_| RunnerError::Journal)?, - ) - .map_err(|_| RunnerError::Journal)?; - } - - let socket = RunnerSocket::bind(&paths, consumed).map_err(|_| RunnerError::Socket)?; - let audit_sink = Box::new(BrokerAuditSink::new( - &bootstrap, - env::var_os("D2B_BROKER_SOCKET_PATH") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/run/d2b/priv.sock")), - )?); - let effect_sink = Box::new(BrokerEffectSink::new( - &bootstrap, - env::var_os("D2B_BROKER_SOCKET_PATH") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/run/d2b/priv.sock")), - )?); - let mut runtime = Runtime { - bootstrap, - operation, - _lock: lock, - paths, - socket, - audit_sink, - effect_sink, - _host_lock: host_lock, - }; - runtime.start_apply()?; - runtime.serve() -} - -fn load_existing_operation( - paths: &RunnerPaths, - bootstrap: &RunnerBootstrap, -) -> Result, RunnerError> { - let (persisted_bootstrap, records) = match d2b_cutover::load_journal(&paths.journal) { - Ok(value) => value, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(_) => return Err(RunnerError::Journal), - }; - if !bootstrap_restart_binding_matches(&persisted_bootstrap, bootstrap) { - return Err(RunnerError::Replay); - } - let binding = JournalBinding::new( - bootstrap.request.operation_id().clone(), - bootstrap.request.revision_plan_id().clone(), - bootstrap.request.request_digest().clone(), - ); - let journal = Journal::from_bytes(binding, &records).map_err(|_| RunnerError::Journal)?; - Operation::reopen(bootstrap.request.clone(), &bootstrap.preview, journal) - .map(|operation| Some((persisted_bootstrap, operation))) - .map_err(|_| RunnerError::Journal) -} - -fn bootstrap_restart_binding_matches( - persisted: &RunnerBootstrap, - supplied: &RunnerBootstrap, -) -> bool { - // The broker may rotate the time-bound capability after the prior runner - // is confirmed dead; all operation and recovery bindings remain exact. - persisted.request == supplied.request - && persisted.preview == supplied.preview - && persisted.recovery == supplied.recovery - && persisted.host_digest == supplied.host_digest - && consent_restart_binding_matches(&persisted.consent, &supplied.consent) - && consent_restart_binding_matches( - &persisted.destructive_consent, - &supplied.destructive_consent, - ) -} - -fn consent_restart_binding_matches( - persisted: &Option, - supplied: &Option, -) -> bool { - match (persisted, supplied) { - (None, None) => true, - (Some(persisted), Some(supplied)) => { - persisted.binding() == supplied.binding() - && persisted.issued_at_ms() == supplied.issued_at_ms() - && persisted.expires_at_ms() == supplied.expires_at_ms() - } - _ => false, - } -} - -fn bootstrap_fd_from_env(value: Option<&str>) -> Result { - let value = value.ok_or(RunnerError::Bootstrap)?; - let fd = value.parse::().map_err(|_| RunnerError::Bootstrap)?; - if fd < RUNNER_BOOTSTRAP_FD { - return Err(RunnerError::Bootstrap); - } - Ok(fd) -} - -fn secure_bootstrap_fd(fd: i32) -> Result<(), RunnerError> { - if fd < RUNNER_BOOTSTRAP_FD { - return Err(RunnerError::Bootstrap); - } - let flags = fcntl(fd, FcntlArg::F_GETFD).map_err(|_| RunnerError::Bootstrap)?; - let secured_flags = FdFlag::from_bits_truncate(flags) | FdFlag::FD_CLOEXEC; - fcntl(fd, FcntlArg::F_SETFD(secured_flags)).map_err(|_| RunnerError::Bootstrap)?; - let verified_flags = fcntl(fd, FcntlArg::F_GETFD).map_err(|_| RunnerError::Bootstrap)?; - if !FdFlag::from_bits_truncate(verified_flags).contains(FdFlag::FD_CLOEXEC) { - return Err(RunnerError::Bootstrap); - } - Ok(()) -} - -fn read_bootstrap_fd(fd: i32) -> Result, RunnerError> { - if fd < RUNNER_BOOTSTRAP_FD { - return Err(RunnerError::Bootstrap); - } - let result = (|| { - secure_bootstrap_fd(fd)?; - let path = format!("/proc/self/fd/{fd}"); - let mut file = OpenOptions::new() - .read(true) - .custom_flags(libc::O_CLOEXEC) - .open(path) - .map_err(|_| RunnerError::Bootstrap)?; - let mut bytes = Vec::new(); - file.by_ref() - .take((MAX_RUNNER_FRAME_BYTES + 1) as u64) - .read_to_end(&mut bytes) - .map_err(|_| RunnerError::Bootstrap)?; - if bytes.len() > MAX_RUNNER_FRAME_BYTES { - return Err(RunnerError::Bootstrap); - } - Ok(bytes) - })(); - let close_result = nix::unistd::close(fd); - match (result, close_result) { - (Ok(bytes), Ok(())) => Ok(bytes), - _ => Err(RunnerError::Bootstrap), - } -} - -fn now_ms() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() - .try_into() - .unwrap_or(u64::MAX) -} - -trait AuditSink { - fn publish( - &mut self, - transition: CutoverAuditTransition, - phase: u8, - reason_digest: Option, - ) -> Result; -} - -trait EffectSink { - fn execute( - &mut self, - request: &EffectRequest, - phase: CutoverPhase, - handoff: Option, - payload: Option, - ) -> Result; - - fn admission( - &mut self, - ) -> Result { - Err(EffectSinkError::NotAllowed) - } - - fn verify( - &mut self, - expected: &RunnerVerificationInput, - ) -> Result { - let _ = expected; - Err(EffectSinkError::NotAllowed) - } -} - -#[derive(Debug)] -enum AuditSinkError { - Unavailable, - Protocol, -} - -#[derive(Debug)] -enum EffectSinkError { - Unavailable, - Protocol, - NotAllowed, -} - -struct BrokerAuditSink { - socket_path: PathBuf, - operation_id: BundleOpId, - capability_digest: CanonicalAuditDigest, - request_digest: CanonicalAuditDigest, -} - -impl BrokerAuditSink { - fn new(bootstrap: &RunnerBootstrap, socket_path: PathBuf) -> Result { - let operation_id = BundleOpId::new(bootstrap.request.operation_id().as_str()); - let capability_digest = - CanonicalAuditDigest::parse(bootstrap.capability.binding_digest().as_str().to_owned()) - .map_err(|_| RunnerError::Audit)?; - let request_digest = - CanonicalAuditDigest::parse(bootstrap.request.request_digest().as_str().to_owned()) - .map_err(|_| RunnerError::Audit)?; - Ok(Self { - socket_path, - operation_id, - capability_digest, - request_digest, - }) - } -} - -impl AuditSink for BrokerAuditSink { - fn publish( - &mut self, - transition: CutoverAuditTransition, - phase: u8, - reason_digest: Option, - ) -> Result { - let reason_digest = reason_digest - .map(|digest| CanonicalAuditDigest::parse(digest.as_str().to_owned())) - .transpose() - .map_err(|_| AuditSinkError::Protocol)?; - let request = BrokerRequest::CutoverAudit(CutoverAuditRequest { - operation_id: self.operation_id.clone(), - phase, - transition, - request_digest: self.request_digest.clone(), - reason_digest, - }); - let envelope = BrokerRequestEnvelope { - request, - caller_role: BrokerCallerRole::CutoverRunner { - operation_id: self.operation_id.clone(), - capability_digest: self.capability_digest.clone(), - }, - test_peer_uid: None, - audit_join: None, - }; - let response = broker_round_trip(&self.socket_path, &envelope)?; - let BrokerResponse::CutoverAudit(response) = response else { - return Err(AuditSinkError::Protocol); - }; - AuditEvidence::durable(response.record_id.as_str().to_owned()) - .map_err(|_| AuditSinkError::Protocol) - } -} - -struct BrokerEffectSink { - socket_path: PathBuf, - operation_id: BundleOpId, - capability_digest: CanonicalAuditDigest, - request_digest: CanonicalAuditDigest, - authority: CutoverEffectAuthority, - system_artifact_id: Option, - source_system_artifact_id: Option, -} - -struct BrokerEffectRoundTrip { - effect: CutoverEffectKind, - effect_id: BundleOpId, - phase: CutoverPhase, - replay_class: CutoverReplayClass, - identity: Option, - handoff: Option, - payload: Option, -} - -impl BrokerEffectSink { - fn new(bootstrap: &RunnerBootstrap, socket_path: PathBuf) -> Result { - let operation_id = BundleOpId::new(bootstrap.request.operation_id().as_str()); - let capability_digest = - CanonicalAuditDigest::parse(bootstrap.capability.binding_digest().as_str().to_owned()) - .map_err(|_| RunnerError::Audit)?; - let request_digest = - CanonicalAuditDigest::parse(bootstrap.request.request_digest().as_str().to_owned()) - .map_err(|_| RunnerError::Audit)?; - let authority = match bootstrap.request.operation_kind() { - OperationKind::Cutover => CutoverEffectAuthority::Cutover, - OperationKind::ScopedReset(d2b_cutover::ResetScope::Zone) => { - CutoverEffectAuthority::ResetZone - } - OperationKind::ScopedReset(d2b_cutover::ResetScope::Provider) => { - CutoverEffectAuthority::ResetProvider - } - OperationKind::ScopedReset(d2b_cutover::ResetScope::Guest) => { - CutoverEffectAuthority::ResetGuest - } - }; - let system_artifact_id = bootstrap - .request - .system_artifact_id() - .map(|id| d2b_contracts_resource::v3::ArtifactId::parse(id.as_str().to_owned())) - .transpose() - .map_err(|_| RunnerError::Bootstrap)?; - let source_system_artifact_id = bootstrap - .request - .source_system_artifact_id() - .map(|id| d2b_contracts_resource::v3::ArtifactId::parse(id.as_str().to_owned())) - .transpose() - .map_err(|_| RunnerError::Bootstrap)?; - Ok(Self { - socket_path, - operation_id, - capability_digest, - request_digest, - authority, - system_artifact_id, - source_system_artifact_id, - }) - } - - fn round_trip_effect( - &self, - request: &EffectRequest, - phase: CutoverPhase, - handoff: Option, - payload: Option, - ) -> Result { - let effect = cutover_effect_kind(request.kind()).ok_or(EffectSinkError::NotAllowed)?; - self.round_trip_request(BrokerEffectRoundTrip { - effect, - effect_id: BundleOpId::new(request.effect_id().as_str()), - phase, - replay_class: match request.replay_class() { - ReplayClass::Repeatable => CutoverReplayClass::Repeatable, - ReplayClass::ReopenByJournaledIdentity => { - CutoverReplayClass::ReopenByJournaledIdentity - } - ReplayClass::QuarantineOnly => CutoverReplayClass::QuarantineOnly, - }, - identity: request - .journaled_identity() - .map(|identity| BundleOpId::new(identity.as_str())), - handoff, - payload, - }) - } - - fn round_trip_request( - &self, - request: BrokerEffectRoundTrip, - ) -> Result { - if !self.authority.permits(request.effect) { - return Err(EffectSinkError::NotAllowed); - } - if request.effect == CutoverEffectKind::ClosureActivation && request.handoff.is_none() { - return Err(EffectSinkError::Protocol); - } - let response = broker_round_trip( - &self.socket_path, - &BrokerRequestEnvelope { - request: BrokerRequest::CutoverEffect(CutoverEffectRequest { - operation_id: self.operation_id.clone(), - authority: self.authority, - phase: request.phase.number(), - effect_id: request.effect_id, - effect: request.effect, - replay_class: request.replay_class, - request_digest: self.request_digest.clone(), - capability_digest: self.capability_digest.clone(), - identity: request.identity, - handoff: request.handoff, - payload: request.payload, - }), - caller_role: BrokerCallerRole::CutoverRunner { - operation_id: self.operation_id.clone(), - capability_digest: self.capability_digest.clone(), - }, - test_peer_uid: None, - audit_join: None, - }, - ) - .map_err(|_| EffectSinkError::Unavailable)?; - let BrokerResponse::CutoverEffect(response) = response else { - return Err(EffectSinkError::Protocol); - }; - Ok(response) - } -} - -impl EffectSink for BrokerEffectSink { - fn execute( - &mut self, - request: &EffectRequest, - phase: CutoverPhase, - handoff: Option, - payload: Option, - ) -> Result { - let response = self.round_trip_effect(request, phase, handoff, payload)?; - let effect = match response.outcome { - CutoverEffectOutcome::Succeeded => match response.identity { - Some(identity) => EffectEvidence::succeeded_with_identity(identity.as_str()) - .map_err(|_| EffectSinkError::Protocol)?, - None => EffectEvidence::succeeded(), - }, - CutoverEffectOutcome::Failed => EffectEvidence::failed(), - CutoverEffectOutcome::Ambiguous => EffectEvidence::ambiguous(), - }; - let audit = AuditEvidence::durable(response.audit_record_id.as_str().to_owned()) - .map_err(|_| EffectSinkError::Protocol)?; - Ok(CompletionEvidence { effect, audit }) - } - - fn admission( - &mut self, - ) -> Result { - let request = d2b_contracts_broker::broker_wire::CutoverAdmissionRequest { - system_artifact_id: self.system_artifact_id.clone(), - source_system_artifact_id: self.source_system_artifact_id.clone(), - }; - let response = self.round_trip_request(BrokerEffectRoundTrip { - effect: CutoverEffectKind::ApplyAdmission, - effect_id: BundleOpId::new("apply-admission"), - phase: CutoverPhase::Disposition, - replay_class: CutoverReplayClass::Repeatable, - identity: None, - handoff: None, - payload: Some(CutoverEffectPayload::ApplyAdmission(request)), - })?; - response.admission.ok_or(EffectSinkError::Protocol) - } - - fn verify( - &mut self, - expected: &RunnerVerificationInput, - ) -> Result { - let effect = EffectRequest::new( - d2b_cutover::EffectId::new("phase-9-verification").expect("effect id"), - d2b_cutover::StepId::new("phase-9-verification").expect("step id"), - EffectKind::Verification, - ReplayClass::Repeatable, - None, - ); - let payload = CutoverEffectPayload::Verification( - d2b_contracts_broker::broker_wire::CutoverVerificationRequest { - expected_zone_ids: expected - .zones - .iter() - .map(|zone| BundleOpId::new(zone.zone_id.as_str())) - .collect(), - }, - ); - let response = - self.round_trip_effect(&effect, CutoverPhase::Verification, None, Some(payload))?; - if response.outcome != CutoverEffectOutcome::Succeeded { - return Err(EffectSinkError::Protocol); - } - let Some(observations) = response.verification else { - return Err(EffectSinkError::Protocol); - }; - Ok(RunnerVerificationInput { - zones: observations - .zones - .into_iter() - .map(|zone| { - Ok(RunnerZoneVerification { - zone_id: d2b_cutover::ZoneId::new(zone.zone_id.as_str().to_owned()) - .map_err(|_| EffectSinkError::Protocol)?, - healthy: zone.healthy, - }) - }) - .collect::, _>>()?, - sources_preserved: observations.sources_preserved, - identity_digests_match: observations.identity_digests_match, - candidate_current: observations.candidate_current, - }) - } -} - -fn cutover_effect_kind(kind: EffectKind) -> Option { - Some(match kind { - EffectKind::HostDrain => CutoverEffectKind::HostDrain, - EffectKind::CutoverDisposition => CutoverEffectKind::CutoverDisposition, - EffectKind::ResourceStoreCreate => CutoverEffectKind::ResourceStoreCreate, - EffectKind::ProviderInstall => CutoverEffectKind::ProviderInstall, - EffectKind::ZoneActivation => CutoverEffectKind::ZoneActivation, - EffectKind::GuestActivation => CutoverEffectKind::GuestActivation, - EffectKind::Verification => CutoverEffectKind::Verification, - EffectKind::CutoverFinalization => CutoverEffectKind::CutoverFinalization, - EffectKind::ScopedZoneReset => CutoverEffectKind::ScopedZoneReset, - EffectKind::ScopedProviderReset => CutoverEffectKind::ScopedProviderReset, - EffectKind::ScopedGuestReset => CutoverEffectKind::ScopedGuestReset, - EffectKind::DestroyDurableVolume => CutoverEffectKind::DestroyDurableVolume, - EffectKind::PreserveSource => CutoverEffectKind::PreserveSource, - EffectKind::QuarantineDestination => CutoverEffectKind::QuarantineDestination, - EffectKind::CutoverBroker => return None, - EffectKind::ClosureActivation => CutoverEffectKind::ClosureActivation, - }) -} - -fn broker_round_trip( - socket_path: &std::path::Path, - envelope: &BrokerRequestEnvelope, -) -> Result { - let fd = socket( - AddressFamily::Unix, - SockType::SeqPacket, - SockFlag::SOCK_CLOEXEC, - None, - ) - .map_err(|_| AuditSinkError::Unavailable)?; - let timeout = TimeVal::milliseconds(RUNNER_SOCKET_TIMEOUT.as_millis() as i64); - setsockopt(&fd, sockopt::SendTimeout, &timeout).map_err(|_| AuditSinkError::Unavailable)?; - setsockopt(&fd, sockopt::ReceiveTimeout, &timeout).map_err(|_| AuditSinkError::Unavailable)?; - let address = UnixAddr::new(socket_path).map_err(|_| AuditSinkError::Unavailable)?; - connect(fd.as_raw_fd(), &address).map_err(|_| AuditSinkError::Unavailable)?; - let body = serde_json::to_vec(envelope).map_err(|_| AuditSinkError::Protocol)?; - if body.len() > MAX_FRAME_SIZE { - return Err(AuditSinkError::Protocol); - } - let mut frame = Vec::with_capacity(body.len() + 4); - frame.extend_from_slice(&(body.len() as u32).to_le_bytes()); - frame.extend_from_slice(&body); - let sent = - send(fd.as_raw_fd(), &frame, MsgFlags::empty()).map_err(|_| AuditSinkError::Unavailable)?; - if sent != frame.len() { - return Err(AuditSinkError::Unavailable); - } - let mut response = vec![0_u8; MAX_FRAME_SIZE + 4]; - let received = recv(fd.as_raw_fd(), &mut response, MsgFlags::empty()) - .map_err(|_| AuditSinkError::Unavailable)?; - if received < 4 { - return Err(AuditSinkError::Protocol); - } - let declared = u32::from_le_bytes( - response[..4] - .try_into() - .map_err(|_| AuditSinkError::Protocol)?, - ) as usize; - if declared > MAX_FRAME_SIZE || declared != received - 4 { - return Err(AuditSinkError::Protocol); - } - serde_json::from_slice(&response[4..received]).map_err(|_| AuditSinkError::Protocol) -} - -struct Runtime { - bootstrap: RunnerBootstrap, - operation: Operation, - _lock: std::fs::File, - paths: RunnerPaths, - socket: RunnerSocket, - audit_sink: Box, - effect_sink: Box, - _host_lock: HostLockContract, -} - -impl Runtime { - fn start_apply(&mut self) -> Result<(), RunnerError> { - if self.operation.state() != d2b_cutover::OperationState::Planned { - return Ok(()); - } - let Some(mut consent) = self.bootstrap.consent.clone() else { - return Ok(()); - }; - let mut lock = HostLockContract::new(); - self.operation - .acquire_host_lock(&mut lock) - .map_err(|_| RunnerError::Request)?; - let admission = self - .effect_sink - .admission() - .map_err(|_| RunnerError::Request)?; - if !admission.predicates_hold { - return Err(RunnerError::Request); - } - let context = match ( - self.bootstrap.recovery.clone(), - self.bootstrap.host_digest.clone(), - ) { - (Some(recovery), Some(host_digest)) => ApplyContext::cutover( - now_ms(), - self.bootstrap.request.inventory_digest().clone(), - admission.candidate_current, - admission.markers_valid, - admission.ownership_valid, - recovery, - host_digest, - ), - _ => ApplyContext::reset( - now_ms(), - self.bootstrap.request.inventory_digest().clone(), - admission.candidate_current, - admission.markers_valid, - admission.ownership_valid, - ), - }; - if matches!( - self.bootstrap.request.inventory(), - OperationInventory::Reset(inventory) if inventory.allows_destroy_durable_volumes() - ) { - let Some(mut destructive_consent) = self.bootstrap.destructive_consent.clone() else { - return Err(RunnerError::Request); - }; - destructive_consent - .consume(&self.bootstrap.request.consent_binding(), now_ms()) - .map_err(|_| RunnerError::Request)?; - self.bootstrap.destructive_consent = Some(destructive_consent); - } - let audit = self - .audit_sink - .publish( - CutoverAuditTransition::PhaseStarted, - self.operation.phase().number(), - None, - ) - .map_err(|_| RunnerError::Audit)?; - let previous = self.operation.clone(); - if self.operation.begin_apply(&mut consent, &context).is_err() { - self.operation = previous.clone(); - return Err(RunnerError::Request); - } - if self.persist().is_err() { - self.operation = previous; - return Err(RunnerError::Journal); - } - let _ = audit; - self._host_lock = lock; - Ok(()) - } - - fn serve(&mut self) -> Result<(), RunnerError> { - loop { - let (mut stream, command, peer) = match self.socket.accept_command() { - Ok(accepted) => accepted, - Err(error) - if matches!( - error.kind(), - std::io::ErrorKind::PermissionDenied - | std::io::ErrorKind::InvalidData - | std::io::ErrorKind::UnexpectedEof - | std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::BrokenPipe - ) => - { - // A disconnected or unauthorized client is scoped to one - // accepted stream; it must never terminate the runner. - continue; - } - Err(_) => return Err(RunnerError::Socket), - }; - let response = self.handle(command, peer); - if write_response(&mut stream, &response).is_err() { - continue; - } - if response.error == Some(d2b_cutover::RunnerSocketError::JournalUnavailable) { - return Err(RunnerError::Journal); - } - if self.operation.state().is_terminal() { - return Ok(()); - } - } - } - - fn handle(&mut self, command: RunnerCommand, peer: RunnerPeer) -> RunnerResponse { - match command { - RunnerCommand::Status => RunnerResponse { - accepted: true, - status: Some(self.status()), - error: None, - }, - RunnerCommand::Apply { handoff } => self.apply_closure(handoff), - RunnerCommand::Effect { - effect_id, - step_id, - kind, - replay_class, - advance_to, - identity, - handoff, - payload, - } => { - if kind == EffectKind::ClosureActivation { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - let effect = EffectRequest::new(effect_id, step_id, kind, replay_class, advance_to); - let effect = match identity { - Some(identity) => effect.with_identity(Some(identity), None), - None => effect, - }; - self.dispatch_effect(effect, handoff, payload) - } - RunnerCommand::Rollback { handoff } => { - if self.operation.phase() == CutoverPhase::Disposition { - let Some(handoff) = handoff else { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - }; - if !validate_handoff_contract(&handoff) { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - let identity = match ArtifactId::new(handoff.intent.system_artifact_id.as_str()) - { - Ok(identity) => identity, - Err(_) => { - return self - .failure(d2b_cutover::RunnerSocketError::ArtifactBindingMismatch); - } - }; - if self.bootstrap.request.source_system_artifact_id() != Some(&identity) { - return self - .failure(d2b_cutover::RunnerSocketError::ArtifactBindingMismatch); - } - let restore = EffectRequest::new( - d2b_cutover::EffectId::new("rollback-generation").expect("effect id"), - StepId::new("rollback-generation").expect("step id"), - EffectKind::ClosureActivation, - ReplayClass::ReopenByJournaledIdentity, - None, - ) - .with_identity(Some(identity), None); - let response = self.dispatch_effect(restore, Some(handoff), None); - if !response.accepted { - return response; - } - } - let audit = match self.audit_sink.publish( - CutoverAuditTransition::Terminal, - self.operation.phase().number(), - None, - ) { - Ok(audit) => audit, - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable); - } - }; - let previous = self.operation.clone(); - if self.operation.rollback(audit).is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - RunnerResponse { - accepted: true, - status: Some(self.status()), - error: None, - } - } - RunnerCommand::Verify { observations } => { - let expected = match self.operation.request().inventory() { - OperationInventory::Host(inventory) => RunnerVerificationInput { - zones: inventory - .zone_ids() - .cloned() - .map(|zone_id| RunnerZoneVerification { - zone_id, - healthy: false, - }) - .collect(), - sources_preserved: false, - identity_digests_match: false, - candidate_current: false, - }, - OperationInventory::Reset(_) => { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - }; - let expected_zone_ids = expected - .zones - .iter() - .map(|zone| zone.zone_id.clone()) - .collect::>(); - let supplied_zone_ids = observations - .zones - .iter() - .map(|zone| zone.zone_id.clone()) - .collect::>(); - if observations.zones.len() != expected_zone_ids.len() - || expected_zone_ids != supplied_zone_ids - { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - let observations = match self.effect_sink.verify(&expected) { - Ok(observations) => observations, - Err(EffectSinkError::Unavailable) - | Err(EffectSinkError::Protocol) - | Err(EffectSinkError::NotAllowed) => { - return self.require_external_restore(); - } - }; - if observations - .zones - .iter() - .map(|zone| zone.zone_id.clone()) - .collect::>() - != expected_zone_ids - { - return self.require_external_restore(); - } - if !observations.zones.iter().all(|zone| zone.healthy) - || !observations.sources_preserved - || !observations.identity_digests_match - || !observations.candidate_current - { - return self.require_external_restore(); - } - let audit = match self.audit_sink.publish( - CutoverAuditTransition::PhaseCompleted, - self.operation.phase().number(), - None, - ) { - Ok(audit) => audit, - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable); - } - }; - let input = d2b_cutover::VerificationInput::new( - observations - .zones - .into_iter() - .map(|zone| d2b_cutover::ZoneVerification::new(zone.zone_id, zone.healthy)), - observations.sources_preserved, - observations.identity_digests_match, - audit.is_durable(), - observations.candidate_current, - ); - let previous = self.operation.clone(); - if self.operation.verify(&input).is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - RunnerResponse { - accepted: true, - status: Some(self.status()), - error: None, - } - } - RunnerCommand::Finalize { mut consent, plan } => { - let consent_digest = match consent.digest() { - Ok(digest) => digest, - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::Malformed); - } - }; - let audit = match self.audit_sink.publish( - CutoverAuditTransition::PhaseStarted, - self.operation.phase().number(), - None, - ) { - Ok(audit) => audit, - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable); - } - }; - let previous = self.operation.clone(); - if self - .operation - .begin_finalization(&mut consent, now_ms()) - .is_err() - { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - let artifacts = match plan - .artifacts - .iter() - .map(|artifact| { - d2b_contracts_resource::v3::ArtifactId::parse(artifact.artifact_id.as_str()) - .map_err(|_| d2b_cutover::RunnerSocketError::Malformed) - }) - .collect::, _>>() - { - Ok(artifacts) => artifacts, - Err(error) => return self.failure(error), - }; - let disposition_digest = d2b_cutover::Digest::derive( - "d2b:cutover:finalization-dispositions:v1", - &serde_json::to_vec(&plan).expect("finalization plan serializes"), - ); - let effect = EffectRequest::new( - d2b_cutover::EffectId::new("phase-10-finalization").expect("effect id"), - StepId::new("phase-10-finalization").expect("step id"), - EffectKind::CutoverFinalization, - ReplayClass::QuarantineOnly, - None, - ); - let response = self.dispatch_effect( - effect, - None, - Some(CutoverEffectPayload::Finalization { - artifacts, - disposition_digest: CanonicalAuditDigest::parse( - disposition_digest.as_str().to_owned(), - ) - .expect("canonical disposition digest"), - consent_digest: CanonicalAuditDigest::parse( - consent_digest.as_str().to_owned(), - ) - .expect("canonical consent digest"), - }), - ); - let _ = audit; - response - } - RunnerCommand::Hold { reason } => { - let reason = match HoldReason::new(reason) { - Ok(reason) => reason, - Err(_) => { - return RunnerResponse { - accepted: false, - status: Some(self.status()), - error: Some(d2b_cutover::RunnerSocketError::Malformed), - }; - } - }; - let requested_by = match peer { - RunnerPeer::Owner => self.bootstrap.request.operator_id().clone(), - RunnerPeer::Admin => d2b_cutover::OperatorId::new("admin-peer") - .expect("static operator identity"), - }; - // The reason digest is request metadata; only the broker's - // durable record id is accepted as audit evidence below. - let reason_digest = d2b_cutover::Digest::derive( - "d2b:cutover:hold-reason:v1", - reason.as_str().as_bytes(), - ); - let audit = match self.audit_sink.publish( - CutoverAuditTransition::HoldRequested, - self.operation.phase().number(), - Some(reason_digest), - ) { - Ok(audit) => audit, - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable); - } - }; - let previous = self.operation.clone(); - match self.operation.request_hold(requested_by, reason, audit) { - Ok(()) => {} - Err(d2b_cutover::OperationError::AuditNotDurable) => { - return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable); - } - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - } - - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - RunnerResponse { - accepted: true, - status: Some(self.status()), - error: None, - } - } - RunnerCommand::Resume { fresh_consent } => { - if matches!(peer, RunnerPeer::Admin) { - let expected = self - .bootstrap - .consent - .as_ref() - .and_then(|consent| consent.digest().ok()) - .unwrap_or_else(|| { - d2b_cutover::Digest::derive( - "d2b:cutover:resume-consent", - self.bootstrap.request.request_digest().as_str().as_bytes(), - ) - }); - if fresh_consent.as_ref() != Some(&expected) { - return self.failure(d2b_cutover::RunnerSocketError::OperatorMismatch); - } - } - let admission = match self.effect_sink.admission() { - Ok(admission) if admission.predicates_hold => admission, - Ok(_) | Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - }; - let context = match self.bootstrap.request.operation_kind() { - OperationKind::ScopedReset(_) => ApplyContext::reset( - now_ms(), - self.bootstrap.request.inventory_digest().clone(), - admission.candidate_current, - admission.markers_valid, - admission.ownership_valid, - ), - OperationKind::Cutover => { - let (Some(recovery), Some(host_digest)) = ( - self.bootstrap.recovery.clone(), - self.bootstrap.host_digest.clone(), - ) else { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - }; - ApplyContext::cutover( - now_ms(), - self.bootstrap.request.inventory_digest().clone(), - admission.candidate_current, - admission.markers_valid, - admission.ownership_valid, - recovery, - host_digest, - ) - } - }; - // Resume has the same durability requirement as hold. - let audit = match self.audit_sink.publish( - CutoverAuditTransition::HoldCleared, - self.operation.phase().number(), - None, - ) { - Ok(audit) => audit, - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable); - } - }; - let previous = self.operation.clone(); - match self - .operation - .resume(self.bootstrap.request.operator_id(), &context, audit) - { - Ok(()) => {} - Err(d2b_cutover::OperationError::AuditNotDurable) => { - return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable); - } - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - } - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - RunnerResponse { - accepted: true, - status: Some(self.status()), - error: None, - } - } - } - } - - fn apply_closure( - &mut self, - handoff: d2b_contracts_broker::host_generation::ApplyHostGenerationHandoff, - ) -> RunnerResponse { - let identity = match ArtifactId::new(handoff.intent.system_artifact_id.as_str()) { - Ok(identity) => identity, - Err(_) => { - return self.failure(d2b_cutover::RunnerSocketError::ArtifactBindingMismatch); - } - }; - if !validate_handoff_contract(&handoff) { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - if self.bootstrap.request.system_artifact_id() != Some(&identity) { - return self.failure(d2b_cutover::RunnerSocketError::ArtifactBindingMismatch); - } - if let Err(error) = self.complete_read_only_prefix() { - return self.failure(error); - } - if self.operation.phase() == CutoverPhase::Drain { - let drain = EffectRequest::new( - d2b_cutover::EffectId::new("host-drain").expect("effect id"), - StepId::new("phase-3-host-drain").expect("step id"), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - let response = self.dispatch_effect(drain, None, None); - if !response.accepted { - return response; - } - } - if self.operation.phase() != CutoverPhase::Disposition { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - let effect = EffectRequest::new( - d2b_cutover::EffectId::new("closure-activation").expect("effect id"), - StepId::new("phase-closure-activation").expect("step id"), - EffectKind::ClosureActivation, - ReplayClass::ReopenByJournaledIdentity, - Some(CutoverPhase::ResourceStore), - ) - .with_identity(Some(identity), None); - self.dispatch_effect(effect, Some(handoff), None) - } - - fn complete_read_only_prefix(&mut self) -> Result<(), d2b_cutover::RunnerSocketError> { - if self.operation.phase().number() > CutoverPhase::Inventory.number() { - return Ok(()); - } - let predicates_hold = self - .effect_sink - .admission() - .map_err(|_| d2b_cutover::RunnerSocketError::InvalidTransition)? - .predicates_hold; - if !predicates_hold { - return Err(d2b_cutover::RunnerSocketError::InvalidTransition); - } - while self.operation.phase().number() <= CutoverPhase::Inventory.number() { - let phase = self.operation.phase(); - let audit = self - .audit_sink - .publish(CutoverAuditTransition::PhaseCompleted, phase.number(), None) - .map_err(|_| d2b_cutover::RunnerSocketError::AuditUnavailable)?; - let previous = self.operation.clone(); - if self - .operation - .complete_read_only_phase( - phase, - ReadOnlyEvidence { - predicates_hold, - audit, - }, - ) - .is_err() - { - self.operation = previous; - return Err(d2b_cutover::RunnerSocketError::InvalidTransition); - } - if self.persist().is_err() { - self.operation = previous; - return Err(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - } - Ok(()) - } - - fn effect_phase_allowed(kind: EffectKind, phase: CutoverPhase) -> bool { - match kind { - EffectKind::HostDrain => phase == CutoverPhase::Drain, - EffectKind::CutoverDisposition - | EffectKind::ClosureActivation - | EffectKind::PreserveSource - | EffectKind::QuarantineDestination - | EffectKind::ScopedZoneReset - | EffectKind::ScopedProviderReset - | EffectKind::ScopedGuestReset - | EffectKind::DestroyDurableVolume => phase == CutoverPhase::Disposition, - EffectKind::ResourceStoreCreate => phase == CutoverPhase::ResourceStore, - EffectKind::ProviderInstall => phase == CutoverPhase::ProviderInstall, - EffectKind::ZoneActivation => phase == CutoverPhase::ZoneCutover, - EffectKind::GuestActivation => phase == CutoverPhase::Activation, - EffectKind::Verification => phase == CutoverPhase::Verification, - EffectKind::CutoverFinalization => phase == CutoverPhase::Finalization, - EffectKind::CutoverBroker => false, - } - } - - fn dispatch_effect( - &mut self, - effect: EffectRequest, - handoff: Option, - payload: Option, - ) -> RunnerResponse { - self.dispatch_effect_internal(effect, handoff, payload) - } - - fn dispatch_effect_internal( - &mut self, - effect: EffectRequest, - handoff: Option, - payload: Option, - ) -> RunnerResponse { - if effect.kind() == EffectKind::ClosureActivation { - let Some(handoff) = handoff.as_ref() else { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - }; - if !validate_handoff_contract(handoff) { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - let Ok(identity) = ArtifactId::new(handoff.intent.system_artifact_id.as_str()) else { - return self.failure(d2b_cutover::RunnerSocketError::ArtifactBindingMismatch); - }; - let expected_identity = if effect.effect_id().as_str() == "rollback-generation" { - self.bootstrap.request.source_system_artifact_id() - } else { - self.bootstrap.request.system_artifact_id() - }; - if expected_identity != Some(&identity) { - return self.failure(d2b_cutover::RunnerSocketError::ArtifactBindingMismatch); - } - if effect.journaled_identity() != Some(&identity) { - return self.failure(d2b_cutover::RunnerSocketError::ArtifactBindingMismatch); - } - } - if effect.kind() == EffectKind::DestroyDurableVolume { - let valid = match ( - self.bootstrap.destructive_consent.as_ref(), - payload.as_ref(), - ) { - ( - Some(consent), - Some(CutoverEffectPayload::DestroyDurableVolume { consent_digest, .. }), - ) => consent - .digest() - .ok() - .is_some_and(|digest| digest.as_str() == consent_digest.as_str()), - _ => false, - }; - if !valid { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - } - if !Self::effect_phase_allowed(effect.kind(), self.operation.phase()) { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - let replaying = match self.operation.current_effect() { - Some(current) if current.matches_replay(&effect) => true, - Some(_) => { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - None => false, - }; - if !replaying { - let previous = self.operation.clone(); - if self.operation.start_effect(effect.clone()).is_err() { - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - // The started record is the replay boundary. It must reach the - // root-owned journal before the broker is allowed to mutate anything. - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - if self - .audit_sink - .publish( - CutoverAuditTransition::EffectStarted, - self.operation.phase().number(), - None, - ) - .is_err() - { - return self.retry_or_restore(FailureCode::AuditNotDurable); - } - } - let evidence = - match self - .effect_sink - .execute(&effect, self.operation.phase(), handoff, payload) - { - Ok(evidence) => evidence, - Err(EffectSinkError::Unavailable) - | Err(EffectSinkError::Protocol) - | Err(EffectSinkError::NotAllowed) => { - return self.retry_or_restore(FailureCode::EffectFailed); - } - }; - let outcome = evidence.effect.outcome(); - if *outcome != EffectOutcome::Succeeded { - let failure_code = match outcome { - EffectOutcome::Failed => FailureCode::EffectFailed, - EffectOutcome::Ambiguous => FailureCode::DestinationAmbiguous, - EffectOutcome::Succeeded => unreachable!("successful effect handled above"), - }; - return self.retry_or_restore(failure_code); - } - let previous = self.operation.clone(); - if self - .operation - .complete_effect(effect.effect_id(), evidence) - .is_err() - { - self.operation = previous; - return self.retry_or_restore(FailureCode::EffectFailed); - } - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - RunnerResponse { - accepted: true, - status: Some(self.status()), - error: None, - } - } - - fn retry_or_restore(&mut self, failure_code: FailureCode) -> RunnerResponse { - if self - .operation - .phase() - .is_before_or_at_native_rollback_boundary() - { - let previous = self.operation.clone(); - if self.operation.retry_effect(failure_code).is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - self.require_external_restore() - } - - fn require_external_restore(&mut self) -> RunnerResponse { - let audit = match self.audit_sink.publish( - CutoverAuditTransition::Terminal, - self.operation.phase().number(), - None, - ) { - Ok(audit) => audit, - Err(_) => return self.failure(d2b_cutover::RunnerSocketError::AuditUnavailable), - }; - let previous = self.operation.clone(); - if self.operation.require_external_restore(audit).is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::InvalidTransition); - } - if self.persist().is_err() { - self.operation = previous; - return self.failure(d2b_cutover::RunnerSocketError::JournalUnavailable); - } - RunnerResponse { - accepted: false, - status: Some(self.status()), - error: Some(d2b_cutover::RunnerSocketError::InvalidTransition), - } - } - - fn persist(&self) -> Result<(), RunnerError> { - persist_journal( - &self.paths.journal, - &self.bootstrap, - &self - .operation - .journal_bytes() - .map_err(|_| RunnerError::Journal)?, - ) - .map_err(|_| RunnerError::Journal) - } - - fn status(&self) -> RunnerStatus { - RunnerStatus { - operation_id: self.bootstrap.request.operation_id().clone(), - preview_digest: self.operation.preview_digest().clone(), - state: self.operation.state(), - phase: self.operation.phase(), - hold_active: matches!(self.operation.state(), d2b_cutover::OperationState::Held), - terminal: self.operation.state().is_terminal(), - } - } - - fn failure(&self, error: d2b_cutover::RunnerSocketError) -> RunnerResponse { - RunnerResponse { - accepted: false, - status: Some(self.status()), - error: Some(error), - } - } -} - -fn validate_handoff_contract( - handoff: &d2b_contracts_broker::host_generation::ApplyHostGenerationHandoff, -) -> bool { - if handoff.validate().is_err() - || !matches!( - handoff.caller_role, - d2b_contracts_broker::host_generation::HandoffCallerRole::Lifecycle - ) - || handoff.target.resource_type().as_str() != "Host" - { - return false; - } - let fingerprint = d2b_contracts_broker::host_generation::target_fingerprint( - &handoff.target, - &handoff.intent.system_artifact_id, - handoff.intent.target_generation, - ); - handoff - .intent - .compatibility - .validate_target(handoff.intent.target_generation, fingerprint) - .is_ok() - && handoff - .intent - .compatibility - .begin_handoff( - handoff.intent.source_generation, - handoff.intent.target_generation, - ) - .is_ok() -} - -#[derive(Debug)] -enum RunnerError { - NotRoot, - Session, - Bootstrap, - Replay, - Lock, - Request, - Audit, - Journal, - Socket, -} - -impl std::fmt::Display for RunnerError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(match self { - Self::NotRoot => "runner must start as uid 0", - Self::Session => "runner could not create a new session", - Self::Bootstrap => "bootstrap capability was rejected", - Self::Replay => "bootstrap capability or journal was already consumed", - Self::Lock => "operation lock was unavailable", - Self::Request => "operation request was rejected", - Self::Audit => "cutover audit sink unavailable", - Self::Journal => "journal durability failed", - Self::Socket => "runner socket failed", - }) - } -} - -impl std::error::Error for RunnerError {} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeSet; - use std::io::Write as _; - use std::os::fd::IntoRawFd; - use std::path::Path; - use std::process::Command; - use std::sync::{Arc, Mutex}; - - use d2b_cutover::{ - CandidateId, CutoverPreview, FinalizationArtifact, FinalizationPlan, HoldReason, - HostInventory, HostLockContract, OperationRequest, OperationState, RecoveryAttestation, - RecoveryId, ResetInventory, ResetScope, RevisionPlanId, RunnerSocketError, - RunnerVerificationInput, RunnerZoneVerification, ZoneId, ZoneInventory, - }; - use nix::fcntl::OFlag; - use nix::unistd::{close, pipe2, write}; - - struct UnavailableAuditSink; - - impl AuditSink for UnavailableAuditSink { - fn publish( - &mut self, - _transition: CutoverAuditTransition, - _phase: u8, - _reason_digest: Option, - ) -> Result { - Err(AuditSinkError::Unavailable) - } - } - - struct UnavailableEffectSink; - - impl EffectSink for UnavailableEffectSink { - fn execute( - &mut self, - _request: &EffectRequest, - _phase: CutoverPhase, - _handoff: Option, - _payload: Option, - ) -> Result { - Err(EffectSinkError::Unavailable) - } - - fn admission( - &mut self, - ) -> Result { - Ok(d2b_contracts_broker::broker_wire::CutoverAdmissionResponse { - candidate_current: true, - markers_valid: true, - ownership_valid: true, - predicates_hold: true, - }) - } - - fn verify( - &mut self, - expected: &RunnerVerificationInput, - ) -> Result { - Ok(RunnerVerificationInput { - zones: expected - .zones - .iter() - .map(|zone| RunnerZoneVerification { - zone_id: zone.zone_id.clone(), - healthy: true, - }) - .collect(), - sources_preserved: true, - identity_digests_match: true, - candidate_current: true, - }) - } - } - - struct RefusingAdmissionSink; - - impl EffectSink for RefusingAdmissionSink { - fn execute( - &mut self, - _request: &EffectRequest, - _phase: CutoverPhase, - _handoff: Option, - _payload: Option, - ) -> Result { - Err(EffectSinkError::NotAllowed) - } - - fn admission( - &mut self, - ) -> Result { - Ok(d2b_contracts_broker::broker_wire::CutoverAdmissionResponse { - candidate_current: false, - markers_valid: false, - ownership_valid: false, - predicates_hold: false, - }) - } - } - - struct ScriptedEffectSink { - kinds: Arc>>, - } - - impl EffectSink for ScriptedEffectSink { - fn execute( - &mut self, - request: &EffectRequest, - _phase: CutoverPhase, - _handoff: Option, - _payload: Option, - ) -> Result { - self.kinds - .lock() - .expect("effect call lock") - .push(request.kind()); - let effect = if request.identity_bearing() { - EffectEvidence::succeeded_with_identity( - request - .journaled_identity() - .expect("identity-bearing effect identity") - .as_str(), - ) - .map_err(|_| EffectSinkError::Protocol)? - } else { - EffectEvidence::succeeded() - }; - Ok(CompletionEvidence { - effect, - audit: AuditEvidence::durable("scripted-effect-audit") - .map_err(|_| EffectSinkError::Protocol)?, - }) - } - - fn admission( - &mut self, - ) -> Result { - Ok(d2b_contracts_broker::broker_wire::CutoverAdmissionResponse { - candidate_current: true, - markers_valid: true, - ownership_valid: true, - predicates_hold: true, - }) - } - - fn verify( - &mut self, - expected: &RunnerVerificationInput, - ) -> Result { - Ok(RunnerVerificationInput { - zones: expected - .zones - .iter() - .map(|zone| RunnerZoneVerification { - zone_id: zone.zone_id.clone(), - healthy: true, - }) - .collect(), - sources_preserved: true, - identity_digests_match: true, - candidate_current: true, - }) - } - } - - struct UnhealthyVerificationSink; - - impl EffectSink for UnhealthyVerificationSink { - fn execute( - &mut self, - _request: &EffectRequest, - _phase: CutoverPhase, - _handoff: Option, - _payload: Option, - ) -> Result { - Err(EffectSinkError::Protocol) - } - - fn verify( - &mut self, - expected: &RunnerVerificationInput, - ) -> Result { - Ok(RunnerVerificationInput { - zones: expected - .zones - .iter() - .map(|zone| RunnerZoneVerification { - zone_id: zone.zone_id.clone(), - healthy: false, - }) - .collect(), - sources_preserved: false, - identity_digests_match: false, - candidate_current: false, - }) - } - } - - fn runtime(label: &str) -> Runtime { - runtime_with_audit(label, Box::new(UnavailableAuditSink)) - } - - fn runtime_with_audit(label: &str, audit_sink: Box) -> Runtime { - std::fs::create_dir_all(".scratch").expect("create test scratch root"); - let operation_id = d2b_cutover::OperationId::new("op-audit-boundary").expect("operation"); - let candidate_id = CandidateId::new("candidate-audit-boundary").expect("candidate"); - let revision_plan_id = - d2b_cutover::RevisionPlanId::new("plan-audit-boundary").expect("plan"); - let operator_id = - d2b_cutover::OperatorId::new("operator-audit-boundary").expect("operator"); - let inventory = - ResetInventory::new(ResetScope::Zone, "zone-audit-boundary").expect("inventory"); - let preview = CutoverPreview::new_reset( - operation_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - candidate_id.clone(), - revision_plan_id.clone(), - inventory.clone(), - ) - .expect("preview"); - let request = OperationRequest::new_reset( - operation_id.clone(), - ResetScope::Zone, - candidate_id, - revision_plan_id, - operator_id.clone(), - preview.digest().expect("preview digest"), - inventory, - ) - .expect("request"); - let capability = d2b_cutover::BootstrapCapability::new_with_identity( - operation_id.clone(), - request.candidate_id().clone(), - operator_id, - OperationKind::ScopedReset(ResetScope::Zone), - d2b_cutover::Digest::derive("d2b:test:runner-audit", b"nonce"), - 100, - 200, - 1000, - BTreeSet::new(), - ) - .expect("capability"); - let bootstrap = RunnerBootstrap { - capability, - request: request.clone(), - preview, - consent: None, - destructive_consent: None, - recovery: None, - host_digest: None, - }; - let mut ledger = CapabilityLedger::default(); - let capability_bytes = bootstrap - .capability - .canonical_bytes() - .expect("capability bytes"); - let consumed = d2b_cutover::BootstrapCapability::decode_and_consume( - &capability_bytes, - 150, - &mut ledger, - ) - .expect("consume capability"); - let root = - PathBuf::from(".scratch").join(format!("runner-audit-{label}-{}", std::process::id())); - let paths = RunnerPaths::new(&root, &operation_id); - let lock = acquire_operation_lock(&paths).expect("lock"); - let socket = RunnerSocket::bind(&paths, consumed).expect("socket"); - let operation = Operation::new(request, &bootstrap.preview).expect("engine"); - persist_journal( - &paths.journal, - &bootstrap, - &operation.journal_bytes().expect("initial journal"), - ) - .expect("persist initial journal"); - Runtime { - bootstrap, - operation, - _lock: lock, - paths, - socket, - audit_sink, - effect_sink: Box::new(UnavailableEffectSink), - _host_lock: HostLockContract::new(), - } - } - - fn cutover_runtime_for_apply( - label: &str, - ) -> ( - Runtime, - d2b_contracts_broker::host_generation::ApplyHostGenerationHandoff, - Arc>>, - ) { - std::fs::create_dir_all(".scratch").expect("create test scratch root"); - let operation_id = - d2b_cutover::OperationId::new(format!("op-apply-sequence-{label}")).expect("operation"); - let candidate_id = - CandidateId::new(format!("candidate-apply-sequence-{label}")).expect("candidate"); - let revision_plan_id = - RevisionPlanId::new(format!("plan-apply-sequence-{label}")).expect("plan"); - let operator_id = d2b_cutover::OperatorId::new(format!("operator-apply-sequence-{label}")) - .expect("operator"); - let inventory = HostInventory::build( - [ZoneId::new("zone-apply").expect("zone")], - [ZoneInventory::new("zone-apply", true, []).expect("zone inventory")], - [], - ) - .expect("inventory"); - let system_artifact_id = d2b_cutover::ArtifactId::new("host-system").expect("artifact"); - let preview = CutoverPreview::new( - operation_id.clone(), - OperationKind::Cutover, - candidate_id.clone(), - revision_plan_id.clone(), - inventory.clone(), - None, - ) - .expect("preview") - .with_system_artifact_id(system_artifact_id.clone()) - .with_source_system_artifact_id( - d2b_cutover::ArtifactId::new("source-system").expect("source artifact"), - ); - let preview_digest = preview.digest().expect("preview digest"); - let recovery_now = now_ms(); - let recovery = RecoveryAttestation::new( - RecoveryId::new(format!("recovery-apply-sequence-{label}")).expect("recovery"), - candidate_id.clone(), - d2b_cutover::Digest::derive("d2b:test:host", b"host"), - preview_digest.clone(), - operator_id.clone(), - d2b_cutover::Digest::derive("d2b:test:restore", b"restore"), - recovery_now.saturating_sub(1_000), - recovery_now.saturating_add(600_000), - true, - ) - .expect("recovery"); - let recovery_digest = recovery.digest().expect("recovery digest"); - let request = OperationRequest::new_cutover( - operation_id.clone(), - candidate_id.clone(), - revision_plan_id, - operator_id.clone(), - preview_digest, - recovery_digest, - inventory.clone(), - ) - .expect("request") - .with_system_artifact_id(system_artifact_id) - .expect("artifact-bound request") - .with_source_system_artifact_id( - d2b_cutover::ArtifactId::new("source-system").expect("source artifact"), - ) - .expect("source-artifact-bound request"); - let capability = d2b_cutover::BootstrapCapability::new_with_identity( - operation_id.clone(), - candidate_id, - operator_id.clone(), - OperationKind::Cutover, - d2b_cutover::Digest::derive("d2b:test:apply-sequence", label.as_bytes()), - 100, - 200, - nix::unistd::geteuid().as_raw(), - BTreeSet::new(), - ) - .expect("capability"); - let mut bootstrap = RunnerBootstrap { - capability, - request: request.clone(), - preview, - consent: Some( - d2b_cutover::Consent::issue( - request.consent_binding(), - recovery_now.saturating_sub(1_000), - recovery_now.saturating_add(600_000), - ) - .expect("consent"), - ), - destructive_consent: None, - recovery: Some(recovery.clone()), - host_digest: Some(d2b_cutover::Digest::derive("d2b:test:host", b"host")), - }; - let capability_bytes = bootstrap - .capability - .canonical_bytes() - .expect("capability bytes"); - let mut ledger = CapabilityLedger::default(); - let consumed = d2b_cutover::BootstrapCapability::decode_and_consume( - &capability_bytes, - 150, - &mut ledger, - ) - .expect("consume capability"); - let root = PathBuf::from(".scratch").join(format!( - "runner-apply-sequence-{label}-{}", - std::process::id() - )); - let paths = RunnerPaths::new(&root, &operation_id); - let lock = acquire_operation_lock(&paths).expect("lock"); - let socket = RunnerSocket::bind(&paths, consumed).expect("socket"); - let mut operation = Operation::new(request, &bootstrap.preview).expect("engine"); - let mut host_lock = HostLockContract::new(); - operation - .acquire_host_lock(&mut host_lock) - .expect("host lock"); - operation - .begin_apply( - bootstrap.consent.as_mut().expect("consent"), - &ApplyContext::cutover( - recovery_now, - inventory.digest().expect("inventory digest"), - true, - true, - true, - recovery, - d2b_cutover::Digest::derive("d2b:test:host", b"host"), - ), - ) - .expect("begin apply"); - persist_journal( - &paths.journal, - &bootstrap, - &operation.journal_bytes().expect("journal"), - ) - .expect("persist"); - let handoff = { - let target = d2b_contracts_resource::v3::ResourceRef::parse("Host/host-system").expect("target"); - let artifact = d2b_contracts_resource::v3::ArtifactId::parse("host-system").expect("artifact"); - let fingerprint = - d2b_contracts_broker::host_generation::target_fingerprint(&target, &artifact, 8); - d2b_contracts_broker::host_generation::ApplyHostGenerationHandoff { - caller_role: d2b_contracts_broker::host_generation::HandoffCallerRole::Lifecycle, - target, - intent: d2b_contracts_broker::host_generation::HostGenerationHandoffIntent { - source_generation: 7, - target_generation: 8, - system_artifact_id: artifact, - activation_mode: d2b_contracts_resource::v3::ActivationMode::Switch, - compatibility: - d2b_contracts_broker::host_generation::SourceGenerationCompatibilityFloorV1::new( - 7, - fingerprint, - ) - .expect("compatibility"), - }, - } - }; - let kinds = Arc::new(Mutex::new(Vec::new())); - ( - Runtime { - bootstrap, - operation, - _lock: lock, - paths, - socket, - audit_sink: Box::new(FixedAuditSink), - effect_sink: Box::new(ScriptedEffectSink { - kinds: kinds.clone(), - }), - _host_lock: host_lock, - }, - handoff, - kinds, - ) - } - - #[test] - fn apply_command_runs_host_drain_before_closure_activation() { - let (mut runtime, handoff, kinds) = cutover_runtime_for_apply("apply"); - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(response.accepted, "apply response: {:?}", response.error); - assert_eq!(runtime.operation.phase(), CutoverPhase::ResourceStore); - assert_eq!( - *kinds.lock().expect("effect calls"), - [EffectKind::HostDrain, EffectKind::ClosureActivation] - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn apply_command_refuses_a_handoff_artifact_different_from_the_admitted_request() { - let (mut runtime, mut handoff, kinds) = cutover_runtime_for_apply("mismatch"); - let artifact = d2b_contracts_resource::v3::ArtifactId::parse("other-system").expect("artifact"); - handoff.intent.system_artifact_id = artifact.clone(); - handoff.intent.compatibility = - d2b_contracts_broker::host_generation::SourceGenerationCompatibilityFloorV1::new( - handoff.intent.source_generation, - d2b_contracts_broker::host_generation::target_fingerprint( - &handoff.target, - &artifact, - handoff.intent.target_generation, - ), - ) - .expect("compatibility"); - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(!response.accepted); - assert_eq!( - response.error, - Some(RunnerSocketError::ArtifactBindingMismatch) - ); - assert!(kinds.lock().expect("effect calls").is_empty()); - assert_eq!( - runtime.operation.phase(), - CutoverPhase::Preflight, - "mismatched handoff must refuse before any effect" - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn bootstrap_refuses_a_source_artifact_different_from_the_preview() { - let (runtime, _handoff, _kinds) = cutover_runtime_for_apply("src"); - let mut bootstrap = runtime.bootstrap.clone(); - let request = bootstrap - .request - .clone() - .with_source_system_artifact_id( - d2b_cutover::ArtifactId::new("other-source").expect("source artifact"), - ) - .expect("source artifact binding"); - let now = now_ms(); - bootstrap.consent = Some( - d2b_cutover::Consent::issue( - request.consent_binding(), - now.saturating_sub(1_000), - now.saturating_add(600_000), - ) - .expect("consent"), - ); - bootstrap.request = request; - assert_eq!( - bootstrap.validate(), - Err(d2b_cutover::RunnerCapabilityError::Preview) - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn apply_command_refuses_a_non_lifecycle_handoff_before_host_drain() { - let (mut runtime, mut handoff, kinds) = cutover_runtime_for_apply("role-mismatch"); - handoff.caller_role = d2b_contracts_broker::host_generation::HandoffCallerRole::Admin; - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(!response.accepted); - assert_eq!(response.error, Some(RunnerSocketError::InvalidTransition)); - assert!(kinds.lock().expect("effect calls").is_empty()); - assert_eq!(runtime.operation.phase(), CutoverPhase::Preflight); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn apply_command_refuses_a_guest_target_before_read_only_progress() { - let (mut runtime, mut handoff, kinds) = cutover_runtime_for_apply("guest-target"); - handoff.target = - d2b_contracts_resource::v3::ResourceRef::parse("Guest/guest-system").expect("guest target"); - handoff.intent.compatibility = - d2b_contracts_broker::host_generation::SourceGenerationCompatibilityFloorV1::new( - handoff.intent.source_generation, - d2b_contracts_broker::host_generation::target_fingerprint( - &handoff.target, - &handoff.intent.system_artifact_id, - handoff.intent.target_generation, - ), - ) - .expect("compatibility"); - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(!response.accepted); - assert_eq!(response.error, Some(RunnerSocketError::InvalidTransition)); - assert!(kinds.lock().expect("effect calls").is_empty()); - assert_eq!(runtime.operation.phase(), CutoverPhase::Preflight); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn closure_activation_rejects_a_journal_identity_different_from_the_handoff() { - let (mut runtime, handoff, kinds) = cutover_runtime_for_apply("effect-mis"); - runtime - .complete_read_only_prefix() - .expect("read-only prefix"); - let drain = EffectRequest::new( - d2b_cutover::EffectId::new("host-drain-effect-mismatch").expect("effect id"), - StepId::new("host-drain-effect-mismatch").expect("step id"), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - assert!( - runtime.dispatch_effect(drain, None, None).accepted, - "host drain" - ); - let mismatched_identity = d2b_cutover::ArtifactId::new("other-system").expect("artifact"); - let effect = EffectRequest::new( - d2b_cutover::EffectId::new("closure-effect-mismatch").expect("effect id"), - StepId::new("closure-effect-mismatch").expect("step id"), - EffectKind::ClosureActivation, - ReplayClass::ReopenByJournaledIdentity, - Some(CutoverPhase::ResourceStore), - ) - .with_identity(Some(mismatched_identity), None); - let response = runtime.dispatch_effect(effect, Some(handoff), None); - assert!(!response.accepted); - assert_eq!( - response.error, - Some(RunnerSocketError::ArtifactBindingMismatch) - ); - assert_eq!( - *kinds.lock().expect("effect calls"), - [EffectKind::HostDrain] - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn generic_closure_activation_effects_are_rejected_before_dispatch() { - let (mut runtime, handoff, kinds) = cutover_runtime_for_apply("gclose"); - let identity = d2b_cutover::ArtifactId::new("other-system").expect("artifact"); - let response = runtime.handle( - RunnerCommand::Effect { - effect_id: d2b_cutover::EffectId::new("gclose").expect("effect id"), - step_id: StepId::new("gclose").expect("step id"), - kind: EffectKind::ClosureActivation, - replay_class: ReplayClass::ReopenByJournaledIdentity, - advance_to: None, - identity: Some(identity), - handoff: Some(handoff), - payload: None, - }, - RunnerPeer::Owner, - ); - assert!(!response.accepted); - assert_eq!(response.error, Some(RunnerSocketError::InvalidTransition)); - assert!(kinds.lock().expect("effect calls").is_empty()); - assert_eq!(runtime.operation.phase(), CutoverPhase::Preflight); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn failed_host_drain_leaves_native_rollback_available() { - let (mut runtime, _handoff, _kinds) = cutover_runtime_for_apply("drain-failure"); - runtime - .complete_read_only_prefix() - .expect("read-only prefix"); - runtime.effect_sink = Box::new(UnavailableEffectSink); - let drain = EffectRequest::new( - d2b_cutover::EffectId::new("host-drain-failure").expect("effect id"), - StepId::new("phase-3-host-drain-failure").expect("step id"), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - let response = runtime.dispatch_effect(drain, None, None); - assert!(!response.accepted); - assert_eq!(runtime.operation.phase(), CutoverPhase::Drain); - assert_eq!( - runtime.operation.state(), - OperationState::Applying(CutoverPhase::Drain) - ); - assert!(runtime.operation.current_effect().is_none()); - let response = runtime.handle(RunnerCommand::Rollback { handoff: None }, RunnerPeer::Owner); - assert!(response.accepted, "rollback: {:?}", response.error); - assert_eq!(runtime.operation.state(), OperationState::RolledBack); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn stale_admission_refuses_before_host_drain() { - let (mut runtime, handoff, kinds) = cutover_runtime_for_apply("stale"); - runtime.effect_sink = Box::new(RefusingAdmissionSink); - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(!response.accepted); - assert_eq!( - response.error, - Some(d2b_cutover::RunnerSocketError::InvalidTransition) - ); - assert_eq!(runtime.operation.phase(), CutoverPhase::Preflight); - assert!(kinds.lock().expect("effect calls").is_empty()); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn existing_journal_is_adopted_after_restart() { - let (runtime, _handoff, _kinds) = cutover_runtime_for_apply("adopt"); - let (_, adopted) = load_existing_operation(&runtime.paths, &runtime.bootstrap) - .expect("load existing operation") - .expect("journal should be adopted"); - assert_eq!(adopted.phase(), CutoverPhase::Preflight); - assert_eq!( - adopted.state(), - OperationState::Applying(CutoverPhase::Preflight) - ); - assert!(adopted.current_effect().is_none()); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn restart_binding_allows_a_rotated_capability_for_the_same_operation() { - let runtime = cutover_runtime_for_apply("adopt-rotated").0; - let mut supplied = runtime.bootstrap.clone(); - supplied.capability = d2b_cutover::BootstrapCapability::new_with_identity( - supplied.request.operation_id().clone(), - supplied.request.candidate_id().clone(), - supplied.request.operator_id().clone(), - supplied.request.operation_kind(), - d2b_cutover::Digest::derive("d2b:test:rotated-capability", b"nonce"), - 100, - 200, - 1000, - BTreeSet::new(), - ) - .expect("rotated capability"); - assert!(bootstrap_restart_binding_matches( - &runtime.bootstrap, - &supplied - )); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn restart_binding_ignores_only_consent_consumption_state() { - let runtime = runtime("restart-consent-binding"); - let issued = - d2b_cutover::Consent::issue(runtime.bootstrap.request.consent_binding(), 100, 200) - .expect("consent"); - let mut consumed = issued.clone(); - consumed - .consume(&runtime.bootstrap.request.consent_binding(), 150) - .expect("consume consent"); - assert!(consent_restart_binding_matches( - &Some(issued), - &Some(consumed) - )); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn rollback_after_host_drain_restores_the_bound_generation_before_closing() { - let (mut runtime, mut handoff, kinds) = cutover_runtime_for_apply("rollback"); - let source_artifact = - d2b_contracts_resource::v3::ArtifactId::parse("source-system").expect("source artifact"); - handoff.intent.source_generation = 7; - handoff.intent.target_generation = 8; - handoff.intent.system_artifact_id = source_artifact.clone(); - let fingerprint = d2b_contracts_broker::host_generation::target_fingerprint( - &handoff.target, - &source_artifact, - handoff.intent.target_generation, - ); - handoff.intent.compatibility = - d2b_contracts_broker::host_generation::SourceGenerationCompatibilityFloorV1::new( - handoff.intent.source_generation, - fingerprint, - ) - .expect("compatibility"); - runtime - .complete_read_only_prefix() - .expect("read-only prefix"); - let drain = EffectRequest::new( - d2b_cutover::EffectId::new("host-drain").expect("effect id"), - StepId::new("phase-3-host-drain").expect("step id"), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - let response = runtime.dispatch_effect(drain, None, None); - assert!(response.accepted); - let response = runtime.handle( - RunnerCommand::Rollback { - handoff: Some(handoff), - }, - RunnerPeer::Owner, - ); - assert!(response.accepted, "rollback response: {:?}", response.error); - assert_eq!(runtime.operation.state(), OperationState::RolledBack); - assert_eq!( - *kinds.lock().expect("effect calls"), - [EffectKind::HostDrain, EffectKind::ClosureActivation] - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn rollback_refuses_a_handoff_artifact_different_from_the_admitted_request() { - let (mut runtime, mut handoff, kinds) = cutover_runtime_for_apply("rsource"); - let source_artifact = - d2b_contracts_resource::v3::ArtifactId::parse("other-source").expect("artifact"); - handoff.intent.system_artifact_id = source_artifact.clone(); - let fingerprint = d2b_contracts_broker::host_generation::target_fingerprint( - &handoff.target, - &source_artifact, - handoff.intent.target_generation, - ); - handoff.intent.compatibility = - d2b_contracts_broker::host_generation::SourceGenerationCompatibilityFloorV1::new( - handoff.intent.source_generation, - fingerprint, - ) - .expect("compatibility"); - runtime - .complete_read_only_prefix() - .expect("read-only prefix"); - let drain = EffectRequest::new( - d2b_cutover::EffectId::new("host-drain-source-rollback").expect("effect id"), - StepId::new("phase-3-source-rollback").expect("step id"), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - assert!(runtime.dispatch_effect(drain, None, None).accepted); - let response = runtime.handle( - RunnerCommand::Rollback { - handoff: Some(handoff), - }, - RunnerPeer::Owner, - ); - assert!(!response.accepted); - assert_eq!( - response.error, - Some(RunnerSocketError::ArtifactBindingMismatch) - ); - assert!(matches!( - runtime.operation.state(), - OperationState::Applying(_) - )); - assert_eq!( - *kinds.lock().expect("effect calls"), - [EffectKind::HostDrain] - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn rollback_refuses_a_guest_target_before_closure_activation() { - let (mut runtime, mut handoff, kinds) = cutover_runtime_for_apply("rguest"); - let source_artifact = - d2b_contracts_resource::v3::ArtifactId::parse("source-system").expect("artifact"); - handoff.target = - d2b_contracts_resource::v3::ResourceRef::parse("Guest/guest-system").expect("guest target"); - handoff.intent.system_artifact_id = source_artifact.clone(); - handoff.intent.compatibility = - d2b_contracts_broker::host_generation::SourceGenerationCompatibilityFloorV1::new( - handoff.intent.source_generation, - d2b_contracts_broker::host_generation::target_fingerprint( - &handoff.target, - &source_artifact, - handoff.intent.target_generation, - ), - ) - .expect("compatibility"); - runtime - .complete_read_only_prefix() - .expect("read-only prefix"); - let drain = EffectRequest::new( - d2b_cutover::EffectId::new("host-drain-guest-rollback").expect("effect id"), - StepId::new("host-drain-guest-rollback").expect("step id"), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - assert!(runtime.dispatch_effect(drain, None, None).accepted); - let response = runtime.handle( - RunnerCommand::Rollback { - handoff: Some(handoff), - }, - RunnerPeer::Owner, - ); - assert!(!response.accepted); - assert_eq!(response.error, Some(RunnerSocketError::InvalidTransition)); - assert_eq!( - *kinds.lock().expect("effect calls"), - [EffectKind::HostDrain] - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn effect_dispatch_rejects_out_of_order_phase_requests() { - let (mut runtime, _handoff, kinds) = cutover_runtime_for_apply("phase-order"); - runtime - .complete_read_only_prefix() - .expect("read-only prefix"); - let response = runtime.handle( - RunnerCommand::Effect { - effect_id: d2b_cutover::EffectId::new("provider-install-too-early") - .expect("effect id"), - step_id: StepId::new("provider-install-too-early").expect("step id"), - kind: EffectKind::ProviderInstall, - replay_class: ReplayClass::Repeatable, - advance_to: Some(CutoverPhase::ZoneCutover), - identity: None, - handoff: None, - payload: None, - }, - RunnerPeer::Owner, - ); - assert_eq!( - response.error, - Some(d2b_cutover::RunnerSocketError::InvalidTransition) - ); - assert_eq!(runtime.operation.phase(), CutoverPhase::Drain); - assert!(kinds.lock().expect("effect calls").is_empty()); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn typed_effects_progress_the_remaining_u4_phases_in_order() { - let (mut runtime, handoff, _kinds) = cutover_runtime_for_apply("phase-chain"); - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(response.accepted); - for (number, kind, advance_to) in [ - ( - "resource-store", - EffectKind::ResourceStoreCreate, - CutoverPhase::ProviderInstall, - ), - ( - "provider", - EffectKind::ProviderInstall, - CutoverPhase::ZoneCutover, - ), - ("zone", EffectKind::ZoneActivation, CutoverPhase::Activation), - ( - "guest", - EffectKind::GuestActivation, - CutoverPhase::Verification, - ), - ( - "verify", - EffectKind::Verification, - CutoverPhase::Finalization, - ), - ] { - let response = runtime.handle( - RunnerCommand::Effect { - effect_id: d2b_cutover::EffectId::new(format!("effect-{number}")) - .expect("effect id"), - step_id: StepId::new(format!("step-{number}")).expect("step id"), - kind, - replay_class: ReplayClass::Repeatable, - advance_to: Some(advance_to), - identity: None, - handoff: None, - payload: None, - }, - RunnerPeer::Owner, - ); - assert!(response.accepted, "{number}: {:?}", response.error); - assert_eq!(runtime.operation.phase(), advance_to); - } - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn phase_five_effect_failure_publishes_external_restore_required() { - let (mut runtime, handoff, _kinds) = cutover_runtime_for_apply("r5"); - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(response.accepted); - runtime.effect_sink = Box::new(UnavailableEffectSink); - - let response = runtime.handle( - RunnerCommand::Effect { - effect_id: d2b_cutover::EffectId::new("phase-five-failure").expect("effect id"), - step_id: StepId::new("phase-five-failure").expect("step id"), - kind: EffectKind::ResourceStoreCreate, - replay_class: ReplayClass::ReopenByJournaledIdentity, - advance_to: Some(CutoverPhase::ProviderInstall), - identity: Some(ArtifactId::new("store-identity").expect("identity")), - handoff: None, - payload: None, - }, - RunnerPeer::Owner, - ); - assert!(!response.accepted); - assert_eq!( - response.error, - Some(d2b_cutover::RunnerSocketError::InvalidTransition) - ); - assert_eq!(runtime.operation.state(), OperationState::RestoreRequired); - let (_, records) = d2b_cutover::load_journal(&runtime.paths.journal).expect("journal"); - assert!(String::from_utf8_lossy(&records).contains("\"restore-required\"")); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn finalization_requires_separate_consent_and_closes_after_audited_effect() { - let (mut runtime, handoff, _kinds) = cutover_runtime_for_apply("finalization"); - let response = runtime.handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner); - assert!(response.accepted); - for (number, kind, advance_to) in [ - ( - "resource-store-finalization", - EffectKind::ResourceStoreCreate, - CutoverPhase::ProviderInstall, - ), - ( - "provider-finalization", - EffectKind::ProviderInstall, - CutoverPhase::ZoneCutover, - ), - ( - "zone-finalization", - EffectKind::ZoneActivation, - CutoverPhase::Activation, - ), - ( - "guest-finalization", - EffectKind::GuestActivation, - CutoverPhase::Verification, - ), - ] { - let response = runtime.handle( - RunnerCommand::Effect { - effect_id: d2b_cutover::EffectId::new(format!("effect-{number}")) - .expect("effect id"), - step_id: StepId::new(format!("step-{number}")).expect("step id"), - kind, - replay_class: ReplayClass::Repeatable, - advance_to: Some(advance_to), - identity: None, - handoff: None, - payload: None, - }, - RunnerPeer::Owner, - ); - assert!(response.accepted, "{number}: {:?}", response.error); - } - let response = runtime.handle( - RunnerCommand::Verify { - observations: RunnerVerificationInput { - zones: vec![RunnerZoneVerification { - zone_id: ZoneId::new("zone-apply").expect("zone"), - healthy: true, - }], - sources_preserved: true, - identity_digests_match: true, - candidate_current: true, - }, - }, - RunnerPeer::Owner, - ); - assert!(response.accepted, "verify: {:?}", response.error); - assert_eq!(runtime.operation.state(), OperationState::CutoverSucceeded); - let consent = d2b_cutover::FinalizationConsent::issue( - runtime.bootstrap.request.finalization_binding(), - now_ms(), - now_ms().saturating_add(10_000), - ) - .expect("finalization consent"); - let plan = FinalizationPlan { - artifacts: vec![FinalizationArtifact { - artifact_id: d2b_cutover::ArtifactId::new("legacy-one").expect("artifact"), - disposition_digest: d2b_cutover::Digest::derive( - "d2b:test:disposition", - b"legacy-one", - ), - }], - }; - let response = runtime.handle(RunnerCommand::Finalize { consent, plan }, RunnerPeer::Owner); - assert!(response.accepted, "finalization: {:?}", response.error); - assert_eq!(runtime.operation.state(), OperationState::Closed); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn verification_ignores_caller_authored_success_and_restores_on_broker_failure() { - let (mut runtime, handoff, _kinds) = cutover_runtime_for_apply("ver-auth"); - assert!( - runtime - .handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner) - .accepted - ); - for (number, kind, advance_to) in [ - ( - "resource-store-verify-authority", - EffectKind::ResourceStoreCreate, - CutoverPhase::ProviderInstall, - ), - ( - "provider-verify-authority", - EffectKind::ProviderInstall, - CutoverPhase::ZoneCutover, - ), - ( - "zone-verify-authority", - EffectKind::ZoneActivation, - CutoverPhase::Activation, - ), - ( - "guest-verify-authority", - EffectKind::GuestActivation, - CutoverPhase::Verification, - ), - ] { - assert!( - runtime - .handle( - RunnerCommand::Effect { - effect_id: d2b_cutover::EffectId::new(format!("effect-{number}")) - .expect("effect id"), - step_id: StepId::new(format!("step-{number}")).expect("step id"), - kind, - replay_class: ReplayClass::Repeatable, - advance_to: Some(advance_to), - identity: None, - handoff: None, - payload: None, - }, - RunnerPeer::Owner, - ) - .accepted - ); - } - runtime.effect_sink = Box::new(UnhealthyVerificationSink); - let response = runtime.handle( - RunnerCommand::Verify { - observations: RunnerVerificationInput { - zones: vec![RunnerZoneVerification { - zone_id: ZoneId::new("zone-apply").expect("zone"), - healthy: true, - }], - sources_preserved: true, - identity_digests_match: true, - candidate_current: true, - }, - }, - RunnerPeer::Owner, - ); - assert!(!response.accepted); - assert_eq!(runtime.operation.state(), OperationState::RestoreRequired); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn finalization_audit_failure_does_not_advance() { - let (mut runtime, handoff, _kinds) = cutover_runtime_for_apply("fin-audit"); - assert!( - runtime - .handle(RunnerCommand::Apply { handoff }, RunnerPeer::Owner) - .accepted - ); - for (number, kind, advance_to) in [ - ( - "resource-store-finalization-audit", - EffectKind::ResourceStoreCreate, - CutoverPhase::ProviderInstall, - ), - ( - "provider-finalization-audit", - EffectKind::ProviderInstall, - CutoverPhase::ZoneCutover, - ), - ( - "zone-finalization-audit", - EffectKind::ZoneActivation, - CutoverPhase::Activation, - ), - ( - "guest-finalization-audit", - EffectKind::GuestActivation, - CutoverPhase::Verification, - ), - ] { - assert!( - runtime - .handle( - RunnerCommand::Effect { - effect_id: d2b_cutover::EffectId::new(format!("effect-{number}")) - .expect("effect id"), - step_id: StepId::new(format!("step-{number}")).expect("step id"), - kind, - replay_class: ReplayClass::Repeatable, - advance_to: Some(advance_to), - identity: None, - handoff: None, - payload: None, - }, - RunnerPeer::Owner, - ) - .accepted - ); - } - runtime - .operation - .verify(&d2b_cutover::VerificationInput::new( - [d2b_cutover::ZoneVerification::new( - ZoneId::new("zone-apply").expect("zone"), - true, - )], - true, - true, - true, - true, - )) - .expect("verify"); - runtime.audit_sink = Box::new(UnavailableAuditSink); - let consent = d2b_cutover::FinalizationConsent::issue( - runtime.bootstrap.request.finalization_binding(), - now_ms(), - now_ms().saturating_add(10_000), - ) - .expect("consent"); - let response = runtime.handle( - RunnerCommand::Finalize { - consent, - plan: FinalizationPlan { - artifacts: Vec::new(), - }, - }, - RunnerPeer::Owner, - ); - assert_eq!( - response.error, - Some(d2b_cutover::RunnerSocketError::AuditUnavailable) - ); - assert_eq!(runtime.operation.state(), OperationState::CutoverSucceeded); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn hold_refuses_without_privileged_audit_and_does_not_advance() { - let mut runtime = runtime("hold"); - let response = runtime.handle( - RunnerCommand::Hold { - reason: "incident".to_owned(), - }, - RunnerPeer::Owner, - ); - assert!(!response.accepted); - assert_eq!(response.error, Some(RunnerSocketError::AuditUnavailable)); - assert_eq!(runtime.operation.state(), OperationState::Planned); - assert!(runtime.operation.journal().records().is_empty()); - let (_, records) = d2b_cutover::load_journal(&runtime.paths.journal).expect("journal"); - assert!(records.is_empty()); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn resume_refuses_without_privileged_audit_and_does_not_advance() { - let mut runtime = runtime("resume"); - runtime - .operation - .request_hold( - runtime.bootstrap.request.operator_id().clone(), - HoldReason::new("setup").expect("reason"), - // Test fixture only: establish a pre-existing held journal. - AuditEvidence::durable("test-setup-audit").expect("setup evidence"), - ) - .expect("setup hold"); - let journal_bytes = runtime - .operation - .journal_bytes() - .expect("setup journal bytes"); - d2b_cutover::persist_journal(&runtime.paths.journal, &runtime.bootstrap, &journal_bytes) - .unwrap_or_else(|error| panic!("persist setup hold failed: {error}")); - let records_before = runtime.operation.journal().records().len(); - let (_, records_on_disk_before) = - d2b_cutover::load_journal(&runtime.paths.journal).expect("journal before resume"); - let response = runtime.handle( - RunnerCommand::Resume { - fresh_consent: None, - }, - RunnerPeer::Owner, - ); - assert!(!response.accepted); - assert_eq!(response.error, Some(RunnerSocketError::AuditUnavailable)); - assert_eq!(runtime.operation.state(), OperationState::Held); - assert_eq!(runtime.operation.journal().records().len(), records_before); - let (_, records_on_disk_after) = - d2b_cutover::load_journal(&runtime.paths.journal).expect("journal after resume"); - assert_eq!(records_on_disk_after, records_on_disk_before); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - struct FixedAuditSink; - - impl AuditSink for FixedAuditSink { - fn publish( - &mut self, - _transition: d2b_contracts_broker::broker_wire::CutoverAuditTransition, - _phase: u8, - _reason_digest: Option, - ) -> Result { - AuditEvidence::durable("typed-audit-record").map_err(|_| AuditSinkError::Unavailable) - } - } - - #[test] - fn durable_audit_evidence_allows_hold_and_persists_it() { - let mut runtime = runtime_with_audit("durable", Box::new(FixedAuditSink)); - let response = runtime.handle( - RunnerCommand::Hold { - reason: "incident".to_owned(), - }, - RunnerPeer::Owner, - ); - assert!(response.accepted); - assert_eq!(runtime.operation.state(), OperationState::Held); - let (_, records) = d2b_cutover::load_journal(&runtime.paths.journal).expect("journal"); - assert!(!records.is_empty()); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn cutover_resume_revalidates_recovery_and_clears_audited_hold() { - let (mut runtime, _handoff, _kinds) = cutover_runtime_for_apply("resume-cutover"); - runtime.audit_sink = Box::new(FixedAuditSink); - runtime - .operation - .request_hold( - runtime.bootstrap.request.operator_id().clone(), - HoldReason::new("inspect").expect("reason"), - AuditEvidence::durable("hold-audit").expect("hold audit"), - ) - .expect("hold"); - runtime.persist().expect("persist hold"); - - let response = runtime.handle( - RunnerCommand::Resume { - fresh_consent: None, - }, - RunnerPeer::Owner, - ); - assert!(response.accepted, "resume response: {:?}", response.error); - assert_eq!( - runtime.operation.state(), - OperationState::Applying(CutoverPhase::Preflight) - ); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn non_owner_resume_requires_the_bound_fresh_consent_digest() { - let mut runtime = runtime_with_audit("fresh-consent", Box::new(FixedAuditSink)); - runtime - .operation - .request_hold( - runtime.bootstrap.request.operator_id().clone(), - HoldReason::new("inspect").expect("reason"), - AuditEvidence::durable("setup-audit").expect("audit"), - ) - .expect("hold"); - let response = runtime.handle( - RunnerCommand::Resume { - fresh_consent: Some(d2b_cutover::Digest::derive( - "d2b:test:wrong-consent", - b"wrong", - )), - }, - RunnerPeer::Admin, - ); - assert!(!response.accepted); - assert_eq!(response.error, Some(RunnerSocketError::OperatorMismatch)); - assert_eq!(runtime.operation.state(), OperationState::Held); - let _ = std::fs::remove_dir_all(runtime.paths.root()); - } - - #[test] - fn closed_effect_mapping_excludes_the_internal_broker_marker() { - assert_eq!( - cutover_effect_kind(EffectKind::ClosureActivation), - Some(CutoverEffectKind::ClosureActivation) - ); - assert_eq!(cutover_effect_kind(EffectKind::CutoverBroker), None); - assert_eq!( - cutover_effect_kind(EffectKind::ScopedGuestReset), - Some(CutoverEffectKind::ScopedGuestReset) - ); - } - - #[test] - fn inherited_non_cloexec_bootstrap_fd_is_secured_and_consumed() { - let (read_fd, write_fd) = pipe2(OFlag::empty()).expect("pipe"); - write(&write_fd, b"bootstrap").expect("write bootstrap"); - drop(write_fd); - - let raw_fd = read_fd.into_raw_fd(); - let flags = fcntl(raw_fd, FcntlArg::F_GETFD).expect("get fd flags"); - assert_eq!( - flags & libc::FD_CLOEXEC, - 0, - "fixture must start with an inherited descriptor" - ); - - let bytes = read_bootstrap_fd(raw_fd).expect("bootstrap fd"); - assert_eq!(bytes, b"bootstrap"); - assert!( - fcntl(raw_fd, FcntlArg::F_GETFD).is_err(), - "bootstrap descriptor must be closed after consumption" - ); - } - - #[test] - fn missing_or_invalid_bootstrap_fd_refuses() { - assert!(read_bootstrap_fd(-1).is_err()); - - let (read_fd, write_fd) = pipe2(OFlag::empty()).expect("pipe"); - drop(write_fd); - let raw_fd = read_fd.into_raw_fd(); - close(raw_fd).expect("close invalid fixture fd"); - assert!(read_bootstrap_fd(raw_fd).is_err()); - } - - #[test] - fn bootstrap_fd_requires_an_explicit_valid_descriptor() { - assert!(bootstrap_fd_from_env(None).is_err()); - assert!(bootstrap_fd_from_env(Some("not-a-fd")).is_err()); - assert!(bootstrap_fd_from_env(Some("2")).is_err()); - assert_eq!(bootstrap_fd_from_env(Some("3")).expect("fd"), 3); - } - - #[test] - fn secured_bootstrap_fd_does_not_survive_exec() { - let (read_fd, write_fd) = pipe2(OFlag::empty()).expect("pipe"); - write(&write_fd, b"bootstrap").expect("write bootstrap"); - drop(write_fd); - let raw_fd = read_fd.into_raw_fd(); - secure_bootstrap_fd(raw_fd).expect("secure bootstrap fd"); - let flags = fcntl(raw_fd, FcntlArg::F_GETFD).expect("get fd flags"); - assert_ne!(flags & libc::FD_CLOEXEC, 0); - - let shell_path = ["/bin/sh", "/run/current-system/sw/bin/sh"] - .into_iter() - .find(|path| Path::new(path).is_file()) - .expect("shell"); - let status = Command::new(shell_path) - .args([ - "-c", - "test ! -e /proc/self/fd/$1", - "fd-check", - &raw_fd.to_string(), - ]) - .status() - .expect("exec shell"); - assert!( - status.success(), - "secured bootstrap fd leaked across exec: {status}" - ); - close(raw_fd).expect("close bootstrap fd"); - } - - #[test] - fn oversized_bootstrap_fd_refuses_and_closes_descriptor() { - std::fs::create_dir_all(".scratch").expect("create test scratch root"); - let path = PathBuf::from(".scratch") - .join(format!("runner-oversized-bootstrap-{}", std::process::id())); - let mut file = std::fs::File::create(&path).expect("create oversized bootstrap"); - file.write_all(&vec![b'x'; MAX_RUNNER_FRAME_BYTES + 1]) - .expect("write oversized bootstrap"); - drop(file); - let file = std::fs::File::open(&path).expect("open oversized bootstrap"); - let raw_fd = file.into_raw_fd(); - assert!(read_bootstrap_fd(raw_fd).is_err()); - assert!( - fcntl(raw_fd, FcntlArg::F_GETFD).is_err(), - "oversized bootstrap descriptor must be closed" - ); - std::fs::remove_file(path).expect("remove oversized bootstrap"); - } - - #[test] - fn runner_secures_bootstrap_fd_before_reading_or_serving() { - let source = include_str!("d2b-cutover-runner.rs"); - let secure = source - .find("secure_bootstrap_fd(fd)?") - .expect("runner must secure the fd before reading"); - let read = source - .find(".read_to_end(&mut bytes)") - .expect("runner must read bounded bootstrap bytes"); - let startup_read = source - .find("let bootstrap_bytes = read_bootstrap_fd(bootstrap_fd)?;") - .expect("runner must consume the bootstrap during startup"); - let session = source - .find("setsid().map_err(|_| RunnerError::Session)?;") - .expect("runner must create its own session"); - let serve = source - .find("runtime.serve()") - .expect("runner must serve only after bootstrap consumption"); - assert!(secure < read && startup_read < session && startup_read < serve); - assert!(source.contains("FcntlArg::F_SETFD(secured_flags)")); - assert!(source.contains("let close_result = nix::unistd::close(fd);")); - } -} diff --git a/packages/d2b-cutover/src/consent.rs b/packages/d2b-cutover/src/consent.rs deleted file mode 100644 index c15d017055..0000000000 --- a/packages/d2b-cutover/src/consent.rs +++ /dev/null @@ -1,481 +0,0 @@ -//! Strict recovery evidence and single-use consent contracts. - -use std::fmt; - -use d2b_contracts_resource::v3::{ - CanonicalJsonError, CanonicalJsonValue, canonical_digest, canonical_json_bytes, -}; -use serde::{Deserialize, Serialize}; - -use crate::model::{ - ArtifactId, CandidateId, Digest, IdError, OperationId, OperationKind, OperatorId, RecoveryId, -}; - -/// Domain separator for recovery attestation digests. -pub const RECOVERY_DOMAIN: &str = "d2b:cutover:recovery:v1"; -/// Maximum lifetime accepted for one recovery attestation. -pub const MAX_RECOVERY_LIFETIME_MS: u64 = 30 * 24 * 60 * 60 * 1000; - -/// A qualified external full-host recovery point. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RecoveryAttestation { - recovery_id: RecoveryId, - candidate_id: CandidateId, - host_digest: Digest, - preview_digest: Digest, - operator_id: OperatorId, - restore_instructions_digest: Digest, - issued_at_ms: u64, - expires_at_ms: u64, - qualified: bool, -} - -impl RecoveryAttestation { - /// Construct a recovery attestation with bounded integer time. - #[allow(clippy::too_many_arguments)] - pub fn new( - recovery_id: RecoveryId, - candidate_id: CandidateId, - host_digest: Digest, - preview_digest: Digest, - operator_id: OperatorId, - restore_instructions_digest: Digest, - issued_at_ms: u64, - expires_at_ms: u64, - qualified: bool, - ) -> Result { - validate_time_window(issued_at_ms, expires_at_ms)?; - Ok(Self { - recovery_id, - candidate_id, - host_digest, - preview_digest, - operator_id, - restore_instructions_digest, - issued_at_ms, - expires_at_ms, - qualified, - }) - } - - /// Decode strict canonical evidence JSON. - pub fn decode_json(bytes: &[u8]) -> Result { - CanonicalJsonValue::parse(bytes).map_err(ConsentError::CanonicalJson)?; - let value: Self = serde_json::from_slice(bytes).map_err(|_| ConsentError::Json)?; - value.validate_shape()?; - Ok(value) - } - - /// Render canonical evidence JSON. - pub fn canonical_bytes(&self) -> Result, ConsentError> { - canonical_json_bytes(self).map_err(ConsentError::CanonicalJson) - } - - /// Compute the digest bound into consent and the journal request. - pub fn digest(&self) -> Result { - let bytes = self.canonical_bytes()?; - Ok(Digest::parse(canonical_digest(RECOVERY_DOMAIN, &bytes))?) - } - - /// Validate the attestation against one exact operation context. - pub fn validate_for( - &self, - expected_candidate: &CandidateId, - expected_preview: &Digest, - expected_operator: &OperatorId, - expected_host: &Digest, - now_ms: u64, - ) -> Result<(), ConsentError> { - if !self.qualified - || &self.candidate_id != expected_candidate - || &self.preview_digest != expected_preview - || &self.operator_id != expected_operator - || &self.host_digest != expected_host - || now_ms < self.issued_at_ms - || now_ms > self.expires_at_ms - { - return Err(ConsentError::RecoveryMismatch); - } - Ok(()) - } - - /// Borrow the recovery identity. - pub fn recovery_id(&self) -> &RecoveryId { - &self.recovery_id - } - - /// Borrow the bound candidate. - pub fn candidate_id(&self) -> &CandidateId { - &self.candidate_id - } - - /// Borrow the bound preview digest. - pub fn preview_digest(&self) -> &Digest { - &self.preview_digest - } - - /// Borrow the bound operator. - pub fn operator_id(&self) -> &OperatorId { - &self.operator_id - } - - /// Borrow the restore-instructions digest. - pub fn restore_instructions_digest(&self) -> &Digest { - &self.restore_instructions_digest - } - - /// Return the issue time. - pub const fn issued_at_ms(&self) -> u64 { - self.issued_at_ms - } - - /// Return the expiry time. - pub const fn expires_at_ms(&self) -> u64 { - self.expires_at_ms - } - - /// Return whether external qualification succeeded. - pub const fn qualified(&self) -> bool { - self.qualified - } - - fn validate_shape(&self) -> Result<(), ConsentError> { - validate_time_window(self.issued_at_ms, self.expires_at_ms)?; - if !self.qualified { - return Err(ConsentError::NotQualified); - } - Ok(()) - } -} - -/// All values required to bind apply consent to one operation. -/// -/// Cutover admission requires `system_artifact_id`; it remains optional here -/// so reset and pre-binding journals retain their decode compatibility. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ConsentBinding { - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - preview_digest: Digest, - recovery_digest: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - system_artifact_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_system_artifact_id: Option, - operator_id: OperatorId, -} - -impl ConsentBinding { - /// Construct an exact binding. - pub fn new( - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - preview_digest: Digest, - recovery_digest: Option, - operator_id: OperatorId, - ) -> Self { - Self { - operation_id, - operation_kind, - candidate_id, - preview_digest, - recovery_digest, - system_artifact_id: None, - source_system_artifact_id: None, - operator_id, - } - } - - /// Bind an optional system artifact identity to this consent. - pub fn with_system_artifact_id(mut self, system_artifact_id: Option) -> Self { - self.system_artifact_id = system_artifact_id; - self - } - - /// Bind the preserved source artifact identity to this consent. - pub fn with_source_system_artifact_id( - mut self, - source_system_artifact_id: Option, - ) -> Self { - self.source_system_artifact_id = source_system_artifact_id; - self - } - - /// Borrow the operation identity. - pub fn operation_id(&self) -> &OperationId { - &self.operation_id - } - - /// Return the operation kind. - pub const fn operation_kind(&self) -> OperationKind { - self.operation_kind - } - - /// Borrow the candidate identity. - pub fn candidate_id(&self) -> &CandidateId { - &self.candidate_id - } - - /// Borrow the preview digest. - pub fn preview_digest(&self) -> &Digest { - &self.preview_digest - } - - /// Borrow the optional recovery digest. - pub fn recovery_digest(&self) -> Option<&Digest> { - self.recovery_digest.as_ref() - } - - /// Borrow the optional system artifact identity. - pub fn system_artifact_id(&self) -> Option<&ArtifactId> { - self.system_artifact_id.as_ref() - } - - /// Borrow the preserved source artifact identity. - pub fn source_system_artifact_id(&self) -> Option<&ArtifactId> { - self.source_system_artifact_id.as_ref() - } - - /// Borrow the bound operator. - pub fn operator_id(&self) -> &OperatorId { - &self.operator_id - } -} - -/// One single-use apply consent. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct Consent { - binding: ConsentBinding, - issued_at_ms: u64, - expires_at_ms: u64, - consumed: bool, -} - -impl Consent { - /// Decode one strict canonical consent artifact. - pub fn decode_json(bytes: &[u8]) -> Result { - CanonicalJsonValue::parse(bytes).map_err(ConsentError::CanonicalJson)?; - let consent: Self = serde_json::from_slice(bytes).map_err(|_| ConsentError::Json)?; - validate_time_window(consent.issued_at_ms, consent.expires_at_ms)?; - Ok(consent) - } - - /// Compute the digest bound to the apply request. - pub fn digest(&self) -> Result { - Ok(crate::model::Digest::derive( - "d2b:cutover:consent:v1", - &self.canonical_bytes()?, - )) - } - - /// Render canonical consent bytes. - pub fn canonical_bytes(&self) -> Result, ConsentError> { - canonical_json_bytes(self).map_err(ConsentError::CanonicalJson) - } - - /// Issue consent for one exact operation binding. - pub fn issue( - binding: ConsentBinding, - issued_at_ms: u64, - expires_at_ms: u64, - ) -> Result { - validate_time_window(issued_at_ms, expires_at_ms)?; - Ok(Self { - binding, - issued_at_ms, - expires_at_ms, - consumed: false, - }) - } - - /// Consume consent once after exact binding and expiry validation. - pub fn consume(&mut self, expected: &ConsentBinding, now_ms: u64) -> Result<(), ConsentError> { - if self.consumed - || &self.binding != expected - || now_ms < self.issued_at_ms - || now_ms > self.expires_at_ms - { - return Err(ConsentError::Invalid); - } - self.consumed = true; - Ok(()) - } - - /// Return whether this consent was already consumed. - pub const fn is_consumed(&self) -> bool { - self.consumed - } - - /// Return the consent issuance time. - pub const fn issued_at_ms(&self) -> u64 { - self.issued_at_ms - } - - /// Borrow the exact binding. - pub fn binding(&self) -> &ConsentBinding { - &self.binding - } - - /// Return the expiry time. - pub const fn expires_at_ms(&self) -> u64 { - self.expires_at_ms - } -} - -/// A distinct phase-10 finalization binding. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct FinalizationBinding { - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - preview_digest: Digest, - operator_id: OperatorId, -} - -impl FinalizationBinding { - /// Construct the separate phase-10 binding. - pub fn new( - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - preview_digest: Digest, - operator_id: OperatorId, - ) -> Self { - Self { - operation_id, - operation_kind, - candidate_id, - preview_digest, - operator_id, - } - } -} - -/// A separately issued, single-use phase-10 consent. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct FinalizationConsent { - binding: FinalizationBinding, - issued_at_ms: u64, - expires_at_ms: u64, - consumed: bool, -} - -impl FinalizationConsent { - /// Decode one strict canonical finalization consent artifact. - pub fn decode_json(bytes: &[u8]) -> Result { - CanonicalJsonValue::parse(bytes).map_err(ConsentError::CanonicalJson)?; - let consent: Self = serde_json::from_slice(bytes).map_err(|_| ConsentError::Json)?; - validate_time_window(consent.issued_at_ms, consent.expires_at_ms)?; - Ok(consent) - } - - /// Render canonical finalization consent bytes. - pub fn canonical_bytes(&self) -> Result, ConsentError> { - canonical_json_bytes(self).map_err(ConsentError::CanonicalJson) - } - - /// Issue a distinct finalization consent. - pub fn issue( - binding: FinalizationBinding, - issued_at_ms: u64, - expires_at_ms: u64, - ) -> Result { - validate_time_window(issued_at_ms, expires_at_ms)?; - Ok(Self { - binding, - issued_at_ms, - expires_at_ms, - consumed: false, - }) - } - - /// Compute the digest bound to a phase-10 broker effect. - pub fn digest(&self) -> Result { - Ok(Digest::derive( - "d2b:cutover:finalization-consent:v1", - &self.canonical_bytes()?, - )) - } - - /// Consume finalization consent once. - pub fn consume( - &mut self, - expected: &FinalizationBinding, - now_ms: u64, - ) -> Result<(), ConsentError> { - if self.consumed - || &self.binding != expected - || now_ms < self.issued_at_ms - || now_ms > self.expires_at_ms - { - return Err(ConsentError::Invalid); - } - self.consumed = true; - Ok(()) - } - - /// Return whether this consent was consumed. - pub const fn is_consumed(&self) -> bool { - self.consumed - } -} - -/// Strict evidence or consent failure. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ConsentError { - /// Duplicate, fractional, negative, trailing, or malformed JSON. - CanonicalJson(CanonicalJsonError), - /// Typed JSON decoding failed. - Json, - /// A bounded identifier or digest was invalid. - Id(IdError), - /// The time window was outside the bounded ordered range. - InvalidTime, - /// The evidence was not externally qualified. - NotQualified, - /// The recovery evidence did not match the exact operation. - RecoveryMismatch, - /// A consent token was stale, mismatched, expired, or replayed. - Invalid, -} - -impl fmt::Display for ConsentError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::CanonicalJson(_) => "strict canonical JSON rejected", - Self::Json => "typed evidence JSON rejected", - Self::Id(_) => "evidence identity rejected", - Self::InvalidTime => "bounded evidence time rejected", - Self::NotQualified => "recovery evidence is not qualified", - Self::RecoveryMismatch => "recovery evidence mismatch", - Self::Invalid => "consent invalid or already consumed", - }) - } -} - -impl std::error::Error for ConsentError {} - -impl From for ConsentError { - fn from(error: IdError) -> Self { - Self::Id(error) - } -} - -fn validate_time_window(issued_at_ms: u64, expires_at_ms: u64) -> Result<(), ConsentError> { - if expires_at_ms <= issued_at_ms - || expires_at_ms - issued_at_ms > MAX_RECOVERY_LIFETIME_MS - || issued_at_ms > i64::MAX as u64 - || expires_at_ms > i64::MAX as u64 - { - Err(ConsentError::InvalidTime) - } else { - Ok(()) - } -} diff --git a/packages/d2b-cutover/src/finalize.rs b/packages/d2b-cutover/src/finalize.rs deleted file mode 100644 index 023a30fde7..0000000000 --- a/packages/d2b-cutover/src/finalize.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Separate phase-10 finalization contracts. - -use std::fmt; - -use crate::model::{CutoverPhase, FailureCode, OperationKind}; - -/// The only phase that may destroy legacy artifacts. -pub const FINALIZATION_PHASE: CutoverPhase = CutoverPhase::Finalization; - -/// Validate that a finalization request is cutover-only and phase-10 bound. -pub fn require_cutover_finalization( - operation_kind: OperationKind, - phase: CutoverPhase, -) -> Result<(), FinalizationError> { - if !operation_kind.is_cutover() { - return Err(FinalizationError::ResetForbidden); - } - if phase != FINALIZATION_PHASE { - return Err(FinalizationError::WrongPhase); - } - Ok(()) -} - -/// Finalization contract failures. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FinalizationError { - /// Scoped resets cannot invoke cutover finalization. - ResetForbidden, - /// Finalization was requested outside phase 10. - WrongPhase, -} - -impl FinalizationError { - /// Return the stable failure class. - pub const fn code(self) -> FailureCode { - match self { - Self::ResetForbidden => FailureCode::EffectNotAllowed, - Self::WrongPhase => FailureCode::InvalidTransition, - } - } -} - -impl fmt::Display for FinalizationError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::ResetForbidden => "reset cannot invoke cutover finalization", - Self::WrongPhase => "finalization requires phase 10", - }) - } -} - -impl std::error::Error for FinalizationError {} diff --git a/packages/d2b-cutover/src/hold.rs b/packages/d2b-cutover/src/hold.rs deleted file mode 100644 index 7ba714dbc7..0000000000 --- a/packages/d2b-cutover/src/hold.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Cutover-wide incident hold model. - -use std::fmt; - -use crate::model::{HoldReason, OperatorId}; - -/// The current hold state. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum HoldState { - /// No hold is active. - Clear, - /// A hold is pending until the current atomic step completes. - Pending { - /// Operator who requested the hold. - requested_by: OperatorId, - /// Bounded operator reason. - reason: HoldReason, - }, - /// Destructive work is paused. - Active { - /// Operator who requested the hold. - requested_by: OperatorId, - /// Bounded operator reason. - reason: HoldReason, - }, -} - -impl HoldState { - /// Return whether a hold blocks starting another effect. - pub const fn blocks_new_effects(&self) -> bool { - !matches!(self, Self::Clear) - } - - /// Return whether a hold is active after an atomic step. - pub const fn is_active(&self) -> bool { - matches!(self, Self::Active { .. }) - } - - /// Return the operator who requested the hold. - pub fn requested_by(&self) -> Option<&OperatorId> { - match self { - Self::Clear => None, - Self::Pending { requested_by, .. } | Self::Active { requested_by, .. } => { - Some(requested_by) - } - } - } - - /// Return the bounded reason. - pub fn reason(&self) -> Option<&HoldReason> { - match self { - Self::Clear => None, - Self::Pending { reason, .. } | Self::Active { reason, .. } => Some(reason), - } - } -} - -/// Hold transition failures. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HoldError { - /// A new hold was requested after terminal completion. - Terminal, - /// Resume was requested by a different operator. - OperatorMismatch, - /// There was no active hold to resume. - NotActive, -} - -impl fmt::Display for HoldError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::Terminal => "hold is unavailable after terminal completion", - Self::OperatorMismatch => "hold operator mismatch", - Self::NotActive => "no active hold", - }) - } -} - -impl std::error::Error for HoldError {} diff --git a/packages/d2b-cutover/src/inventory.rs b/packages/d2b-cutover/src/inventory.rs deleted file mode 100644 index 1a3bffb4ef..0000000000 --- a/packages/d2b-cutover/src/inventory.rs +++ /dev/null @@ -1,441 +0,0 @@ -//! Host-wide inventory construction and fail-closed classification. - -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt; - -use d2b_contracts_resource::v3::{CanonicalJsonError, CanonicalJsonValue, canonical_json_bytes}; -use serde::{Deserialize, Serialize}; - -use crate::model::{ArtifactId, Digest, Disposition, FailureCode, ZoneId}; - -/// Inventory classes admitted by the cutover contract. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum InventoryClass { - /// A configured Zone identity. - Zone, - /// A declared Guest identity. - Guest, - /// A declared Provider identity. - Provider, - /// TPM marker and NVRAM identity. - TpmIdentity, - /// A durable user-data Volume. - DurableVolume, - /// A closure-only store-view and its gcroot identity. - StoreView, - /// A framework-managed SSH key identity. - SshKey, - /// A managed network ownership marker. - NetworkMarker, - /// A legacy audit chain that must remain historical evidence. - AuditChain, - /// Host runtime metadata. - HostRuntime, - /// Regenerable boot-scoped runtime state. - EphemeralRuntime, - /// An item not recognized by the current migration matrix. - Unclassified, -} - -impl InventoryClass { - /// Return whether the class carries an identity that must survive phase 10. - pub const fn is_identity_bearing(self) -> bool { - matches!( - self, - Self::TpmIdentity - | Self::DurableVolume - | Self::StoreView - | Self::SshKey - | Self::AuditChain - ) - } -} - -/// A single path-free inventory observation. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct InventoryItem { - identity: ArtifactId, - class: InventoryClass, - identity_bearing: bool, - disposition: Disposition, - source_retained_through_phase10: bool, -} - -impl InventoryItem { - /// Construct a classified observation. - pub fn classified( - identity: impl Into, - class: InventoryClass, - disposition: Disposition, - ) -> Result { - let identity = ArtifactId::new(identity).map_err(|_| InventoryError::InvalidIdentity)?; - let identity_bearing = class.is_identity_bearing(); - if identity_bearing && disposition == Disposition::Destroy { - return Err(InventoryError::IdentityArtifactDestroy); - } - Ok(Self { - identity, - class, - identity_bearing, - disposition, - source_retained_through_phase10: identity_bearing - || disposition != Disposition::Destroy, - }) - } - - /// Construct an unclassified item, which always defaults to Preserve. - pub fn unclassified(identity: impl Into) -> Result { - Self::classified( - identity, - InventoryClass::Unclassified, - Disposition::Preserve, - ) - } - - /// Borrow the opaque identity. - pub fn identity(&self) -> &ArtifactId { - &self.identity - } - - /// Return the inventory class. - pub const fn class(&self) -> InventoryClass { - self.class - } - - /// Return the selected disposition. - pub const fn disposition(&self) -> Disposition { - self.disposition - } - - /// Return whether the source is identity-bearing. - pub const fn identity_bearing(&self) -> bool { - self.identity_bearing - } - - /// Return whether the source must remain through phase 10. - pub const fn source_retained_through_phase10(&self) -> bool { - self.source_retained_through_phase10 - } -} - -/// Input item that can explicitly demonstrate the gateway custody boundary. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum InventoryInputItem { - /// A normal path-free item. - Item(InventoryItem), - /// A forbidden attempt to enumerate gateway credentials or gateway audit. - RealmGatewayCredentialAudit(ArtifactId), -} - -impl From for InventoryInputItem { - fn from(value: InventoryItem) -> Self { - Self::Item(value) - } -} - -/// Inventory for one configured Zone. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ZoneInventory { - zone_id: ZoneId, - complete: bool, - items: Vec, -} - -impl ZoneInventory { - /// Construct one Zone observation. - pub fn new( - zone_id: impl Into, - complete: bool, - items: impl IntoIterator, - ) -> Result { - let zone_id = ZoneId::new(zone_id).map_err(|_| InventoryError::InvalidZone)?; - let items = normalize_items(items)?; - Ok(Self { - zone_id, - complete, - items, - }) - } - - /// Construct a complete empty Zone inventory. - pub fn empty(zone_id: impl Into) -> Result { - Self::new(zone_id, true, []) - } - - /// Borrow the Zone identity. - pub fn zone_id(&self) -> &ZoneId { - &self.zone_id - } - - /// Return whether the Zone observation is internally complete. - pub const fn is_complete(&self) -> bool { - self.complete - } - - /// Borrow Zone-local items in canonical order. - pub fn items(&self) -> &[InventoryItem] { - &self.items - } -} - -/// The complete host-wide inventory used by a cutover preview. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HostInventory { - zones: Vec, - shared_items: Vec, -} - -impl HostInventory { - /// Build an all-Zone inventory and reject partial or inconsistent input. - pub fn build( - configured_zones: impl IntoIterator, - observed_zones: impl IntoIterator, - shared_items: impl IntoIterator, - ) -> Result { - let configured = configured_zones.into_iter().collect::>(); - if configured.is_empty() { - return Err(InventoryError::NoConfiguredZones); - } - - let mut zones_by_id = BTreeMap::new(); - for zone in observed_zones { - if !configured.contains(zone.zone_id()) { - return Err(InventoryError::UnexpectedZone); - } - if !zone.is_complete() { - return Err(InventoryError::IncompleteZone(zone.zone_id().clone())); - } - if zones_by_id.insert(zone.zone_id().clone(), zone).is_some() { - return Err(InventoryError::DuplicateZone); - } - } - if zones_by_id.len() != configured.len() - || configured - .iter() - .any(|zone_id| !zones_by_id.contains_key(zone_id)) - { - return Err(InventoryError::PartialZoneInventory); - } - - let shared_items = normalize_items(shared_items)?; - let zones = zones_by_id.into_values().collect::>(); - let mut identities = BTreeSet::new(); - for item in shared_items - .iter() - .chain(zones.iter().flat_map(|zone| zone.items.iter())) - { - if !identities.insert(item.identity().clone()) { - return Err(InventoryError::DuplicateIdentity); - } - } - - Ok(Self { - zones, - shared_items, - }) - } - - /// Return the canonical all-Zone inventory digest. - pub fn digest(&self) -> Result { - let bytes = canonical_json_bytes(self).map_err(InventoryError::CanonicalJson)?; - Ok(Digest::derive("d2b:cutover:inventory:v1", &bytes)) - } - - /// Render exact canonical inventory bytes. - pub fn canonical_bytes(&self) -> Result, InventoryError> { - canonical_json_bytes(self).map_err(InventoryError::CanonicalJson) - } - - /// Decode an inventory through the duplicate-rejecting canonical JSON path. - pub fn decode_json(bytes: &[u8]) -> Result { - CanonicalJsonValue::parse(bytes).map_err(InventoryError::CanonicalJson)?; - let wire: HostInventory = - serde_json::from_slice(bytes).map_err(|_| InventoryError::MalformedJson)?; - let zones = wire - .zones - .into_iter() - .map(|zone| { - ZoneInventory::new( - zone.zone_id.as_str(), - zone.complete, - zone.items.into_iter().map(InventoryInputItem::Item), - ) - }) - .collect::, _>>()?; - let configured = zones - .iter() - .map(|zone| zone.zone_id.clone()) - .collect::>(); - Self::build( - configured, - zones, - wire.shared_items.into_iter().map(InventoryInputItem::Item), - ) - } - - /// Revalidate a deserialized inventory against the all-Zone contract. - pub fn validate(&self) -> Result<(), InventoryError> { - let configured = self - .zones - .iter() - .map(|zone| zone.zone_id.clone()) - .collect::>(); - let zones = self.zones.clone(); - let shared = self - .shared_items - .iter() - .cloned() - .map(InventoryInputItem::Item) - .collect::>(); - Self::build(configured, zones, shared).map(|_| ()) - } - - /// Borrow all configured Zones in canonical order. - pub fn zones(&self) -> &[ZoneInventory] { - &self.zones - } - - /// Borrow shared host items in canonical order. - pub fn shared_items(&self) -> &[InventoryItem] { - &self.shared_items - } - - /// Return every Zone identity. - pub fn zone_ids(&self) -> impl Iterator { - self.zones.iter().map(ZoneInventory::zone_id) - } - - /// Return whether every source artifact remains safe through phase 10. - pub fn sources_retained(&self) -> bool { - self.shared_items - .iter() - .chain(self.zones.iter().flat_map(|zone| zone.items.iter())) - .all(InventoryItem::source_retained_through_phase10) - } - - /// Return all identity-bearing source artifacts. - pub fn identity_bearing_sources(&self) -> Vec<&InventoryItem> { - self.shared_items - .iter() - .chain(self.zones.iter().flat_map(|zone| zone.items.iter())) - .filter(|item| item.identity_bearing()) - .collect() - } -} - -/// Inventory construction failures. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum InventoryError { - /// No configured Zone was supplied. - NoConfiguredZones, - /// A Zone was present more than once. - DuplicateZone, - /// The observed set did not cover every configured Zone. - PartialZoneInventory, - /// An observed Zone was not configured. - UnexpectedZone, - /// A Zone observation was incomplete. - IncompleteZone(ZoneId), - /// An item identity was duplicated. - DuplicateIdentity, - /// An item identity was invalid. - InvalidIdentity, - /// A Zone identity was invalid. - InvalidZone, - /// An identity-bearing source may not be destroyed. - IdentityArtifactDestroy, - /// Gateway credentials and audit stay inside their gateway Guest. - RealmGatewayCredentialAuditForbidden, - /// Canonical encoding failed. - CanonicalJson(CanonicalJsonError), - /// The typed inventory JSON shape was invalid. - MalformedJson, - /// The operation kind and inventory kind did not match. - InventoryKindMismatch, -} - -impl InventoryError { - /// Return the stable fail-closed error class. - pub const fn code(&self) -> FailureCode { - match self { - Self::NoConfiguredZones - | Self::DuplicateZone - | Self::PartialZoneInventory - | Self::UnexpectedZone - | Self::IncompleteZone(_) => FailureCode::InventoryIncomplete, - Self::DuplicateIdentity - | Self::InvalidIdentity - | Self::InvalidZone - | Self::IdentityArtifactDestroy - | Self::InventoryKindMismatch - | Self::MalformedJson - | Self::CanonicalJson(_) => FailureCode::InventoryInconsistent, - Self::RealmGatewayCredentialAuditForbidden => { - FailureCode::GatewayCredentialAuditEnumerationForbidden - } - } - } -} - -impl fmt::Display for InventoryError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::NoConfiguredZones => "no configured Zones", - Self::DuplicateZone => "duplicate Zone", - Self::PartialZoneInventory => "partial Zone inventory", - Self::UnexpectedZone => "unexpected Zone", - Self::IncompleteZone(_) => "incomplete Zone inventory", - Self::DuplicateIdentity => "duplicate inventory identity", - Self::InvalidIdentity => "invalid inventory identity", - Self::InvalidZone => "invalid Zone identity", - Self::IdentityArtifactDestroy => "identity-bearing source cannot be destroyed", - Self::InventoryKindMismatch => "inventory kind mismatch", - Self::MalformedJson => "inventory JSON shape rejected", - Self::RealmGatewayCredentialAuditForbidden => { - "gateway credential and audit enumeration is forbidden" - } - Self::CanonicalJson(_) => "inventory canonicalization failed", - }) - } -} - -impl std::error::Error for InventoryError {} - -fn normalize_items( - items: impl IntoIterator, -) -> Result, InventoryError> { - let mut normalized = Vec::new(); - for item in items { - match item { - InventoryInputItem::Item(mut item) => { - item.identity_bearing |= item.class.is_identity_bearing(); - if item.class == InventoryClass::Unclassified { - item.disposition = Disposition::Preserve; - item.source_retained_through_phase10 = true; - } - if item.identity_bearing && item.disposition == Disposition::Destroy { - return Err(InventoryError::IdentityArtifactDestroy); - } - item.source_retained_through_phase10 |= - item.identity_bearing || item.disposition != Disposition::Destroy; - normalized.push(item); - } - InventoryInputItem::RealmGatewayCredentialAudit(_) => { - return Err(InventoryError::RealmGatewayCredentialAuditForbidden); - } - } - } - normalized.sort_by(|left, right| left.identity().cmp(right.identity())); - let mut identities = BTreeSet::new(); - if normalized - .iter() - .any(|item| !identities.insert(item.identity().clone())) - { - return Err(InventoryError::DuplicateIdentity); - } - Ok(normalized) -} diff --git a/packages/d2b-cutover/src/journal.rs b/packages/d2b-cutover/src/journal.rs deleted file mode 100644 index 021aee46bb..0000000000 --- a/packages/d2b-cutover/src/journal.rs +++ /dev/null @@ -1,783 +0,0 @@ -//! Digest-bound append-only journal model. - -use std::fmt; - -use d2b_contracts_resource::v3::{ - CanonicalJsonError, CanonicalJsonValue, canonical_digest, canonical_json_bytes, -}; -use serde::{Deserialize, Serialize}; - -use crate::model::{ - ArtifactId, AuditRecordId, CutoverPhase, Digest, EffectId, EffectKind, FailureCode, - OperationId, ReplayClass, RevisionPlanId, StepId, TerminalOutcomeKind, -}; - -/// Domain separator for journal record digests. -pub const JOURNAL_DOMAIN: &str = "d2b:cutover:journal-record:v1"; - -/// The closed kinds of journal record. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum JournalRecordKind { - /// Consent was consumed before any mutation. - ConsentConsumed, - /// An effect was durably marked started before mutation. - Started, - /// An effect completed with durable effect and audit evidence. - Completed, - /// A read-only phase completed. - PhaseCompleted, - /// A hold was requested. - HoldRequested, - /// A hold was cleared or resumed. - HoldCleared, - /// An effect failed before the native rollback boundary and may be retried. - RetryableFailure, - /// A terminal result was written once. - Terminal, -} - -/// The binding carried by every journal record. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct JournalBinding { - operation_id: OperationId, - revision_plan_id: RevisionPlanId, - request_digest: Digest, -} - -impl JournalBinding { - /// Construct the immutable journal binding. - pub fn new( - operation_id: OperationId, - revision_plan_id: RevisionPlanId, - request_digest: Digest, - ) -> Self { - Self { - operation_id, - revision_plan_id, - request_digest, - } - } - - /// Borrow the operation identity. - pub fn operation_id(&self) -> &OperationId { - &self.operation_id - } - - /// Borrow the revision-plan identity. - pub fn revision_plan_id(&self) -> &RevisionPlanId { - &self.revision_plan_id - } - - /// Borrow the request digest. - pub fn request_digest(&self) -> &Digest { - &self.request_digest - } -} - -/// One tamper-evident journal record. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct JournalRecord { - sequence: u64, - operation_id: OperationId, - revision_plan_id: RevisionPlanId, - request_digest: Digest, - previous_record_digest: Option, - kind: JournalRecordKind, - phase: CutoverPhase, - step_id: Option, - effect_id: Option, - effect_kind: Option, - next_phase: Option, - replay_class: Option, - identity: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - destination: Option, - audit_record_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - failure_code: Option, - terminal_outcome: Option, - record_digest: Digest, -} - -impl JournalRecord { - /// Return the sequence number. - pub const fn sequence(&self) -> u64 { - self.sequence - } - - /// Return the record kind. - pub const fn kind(&self) -> JournalRecordKind { - self.kind - } - - /// Return the phase attached to the record. - pub const fn phase(&self) -> CutoverPhase { - self.phase - } - - /// Borrow the effect identity, if this record carries one. - pub fn effect_id(&self) -> Option<&EffectId> { - self.effect_id.as_ref() - } - - /// Return the closed effect kind, if this is an effect record. - pub const fn effect_kind(&self) -> Option { - self.effect_kind - } - - /// Return the phase selected after successful effect completion. - pub const fn next_phase(&self) -> Option { - self.next_phase - } - - /// Borrow the step identity, if this record carries one. - pub fn step_id(&self) -> Option<&StepId> { - self.step_id.as_ref() - } - - /// Return the replay class, if this is an effect record. - pub const fn replay_class(&self) -> Option { - self.replay_class - } - - /// Borrow the journaled identity, if present. - pub fn identity(&self) -> Option<&ArtifactId> { - self.identity.as_ref() - } - - /// Borrow the staged destination identity, if present. - pub fn destination(&self) -> Option<&ArtifactId> { - self.destination.as_ref() - } - - /// Return the retryable failure class, if this is a retry record. - pub const fn failure_code(&self) -> Option { - self.failure_code - } - - /// Borrow the durable audit identity, if present. - pub fn audit_record_id(&self) -> Option<&AuditRecordId> { - self.audit_record_id.as_ref() - } - - /// Return the terminal outcome, if this is a terminal record. - pub const fn terminal_outcome(&self) -> Option { - self.terminal_outcome - } - - /// Borrow this record's digest. - pub fn record_digest(&self) -> &Digest { - &self.record_digest - } - - fn unsigned(&self) -> JournalRecordUnsigned { - JournalRecordUnsigned { - sequence: self.sequence, - operation_id: self.operation_id.clone(), - revision_plan_id: self.revision_plan_id.clone(), - request_digest: self.request_digest.clone(), - previous_record_digest: self.previous_record_digest.clone(), - kind: self.kind, - phase: self.phase, - step_id: self.step_id.clone(), - effect_id: self.effect_id.clone(), - effect_kind: self.effect_kind, - next_phase: self.next_phase, - replay_class: self.replay_class, - identity: self.identity.clone(), - destination: self.destination.clone(), - audit_record_id: self.audit_record_id.clone(), - failure_code: self.failure_code, - terminal_outcome: self.terminal_outcome, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(rename_all = "camelCase")] -struct JournalRecordUnsigned { - sequence: u64, - operation_id: OperationId, - revision_plan_id: RevisionPlanId, - request_digest: Digest, - previous_record_digest: Option, - kind: JournalRecordKind, - phase: CutoverPhase, - step_id: Option, - effect_id: Option, - effect_kind: Option, - next_phase: Option, - replay_class: Option, - identity: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - destination: Option, - audit_record_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - failure_code: Option, - terminal_outcome: Option, -} - -/// A pure append-only journal. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Journal { - binding: JournalBinding, - records: Vec, -} - -impl Journal { - /// Start an empty journal for one immutable request binding. - pub const fn new(binding: JournalBinding) -> Self { - Self { - binding, - records: Vec::new(), - } - } - - /// Borrow the immutable journal binding. - pub fn binding(&self) -> &JournalBinding { - &self.binding - } - - /// Borrow records in sequence order. - pub fn records(&self) -> &[JournalRecord] { - &self.records - } - - /// Return the canonical JSONL representation. - pub fn to_bytes(&self) -> Result, JournalError> { - let mut bytes = Vec::new(); - for record in &self.records { - bytes.extend(canonical_json_bytes(record).map_err(JournalError::CanonicalJson)?); - bytes.push(b'\n'); - } - Ok(bytes) - } - - /// Reopen a journal only after proving its chain and request binding. - pub fn from_bytes(binding: JournalBinding, bytes: &[u8]) -> Result { - if bytes.is_empty() { - return Ok(Self::new(binding)); - } - if !bytes.ends_with(b"\n") { - return Err(JournalError::Truncated); - } - - let mut journal = Self::new(binding); - let lines = bytes.split(|byte| *byte == b'\n').collect::>(); - for (index, line) in lines.iter().enumerate() { - if index == lines.len() - 1 { - continue; - } - if line.is_empty() { - return Err(JournalError::Malformed); - } - let parsed = CanonicalJsonValue::parse(line).map_err(JournalError::CanonicalJson)?; - if parsed.to_canonical_bytes() != *line { - return Err(JournalError::NonCanonical); - } - let record: JournalRecord = - serde_json::from_slice(line).map_err(|_| JournalError::Malformed)?; - journal.validate_record(&record)?; - journal.records.push(record); - } - Ok(journal) - } - - /// Append a consent-consumed record. - pub fn append_consent(&mut self, phase: CutoverPhase) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::ConsentConsumed, - phase, - step_id: None, - effect_id: None, - effect_kind: None, - next_phase: None, - replay_class: None, - identity: None, - destination: None, - audit_record_id: None, - failure_code: None, - terminal_outcome: None, - }) - } - - /// Append a started record before an effect is invoked. - #[allow(clippy::too_many_arguments)] - pub fn append_started( - &mut self, - phase: CutoverPhase, - step_id: StepId, - effect_id: EffectId, - effect_kind: EffectKind, - next_phase: Option, - replay_class: ReplayClass, - identity: Option, - ) -> Result<(), JournalError> { - self.append_started_with_destination( - phase, - step_id, - effect_id, - effect_kind, - next_phase, - replay_class, - identity, - None, - ) - } - - /// Append a started record with the staged destination needed for rollback. - #[allow(clippy::too_many_arguments)] - pub fn append_started_with_destination( - &mut self, - phase: CutoverPhase, - step_id: StepId, - effect_id: EffectId, - effect_kind: EffectKind, - next_phase: Option, - replay_class: ReplayClass, - identity: Option, - destination: Option, - ) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::Started, - phase, - step_id: Some(step_id), - effect_id: Some(effect_id), - effect_kind: Some(effect_kind), - next_phase, - replay_class: Some(replay_class), - identity, - destination, - audit_record_id: None, - failure_code: None, - terminal_outcome: None, - }) - } - - /// Append a completed record after both effect and audit durability. - #[allow(clippy::too_many_arguments)] - pub fn append_completed( - &mut self, - phase: CutoverPhase, - step_id: StepId, - effect_id: EffectId, - effect_kind: EffectKind, - next_phase: Option, - replay_class: ReplayClass, - identity: Option, - audit_record_id: AuditRecordId, - ) -> Result<(), JournalError> { - self.append_completed_with_destination( - phase, - step_id, - effect_id, - effect_kind, - next_phase, - replay_class, - identity, - None, - audit_record_id, - ) - } - - /// Append a completed record with the staged destination needed for rollback. - #[allow(clippy::too_many_arguments)] - pub fn append_completed_with_destination( - &mut self, - phase: CutoverPhase, - step_id: StepId, - effect_id: EffectId, - effect_kind: EffectKind, - next_phase: Option, - replay_class: ReplayClass, - identity: Option, - destination: Option, - audit_record_id: AuditRecordId, - ) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::Completed, - phase, - step_id: Some(step_id), - effect_id: Some(effect_id), - effect_kind: Some(effect_kind), - next_phase, - replay_class: Some(replay_class), - identity, - destination, - audit_record_id: Some(audit_record_id), - failure_code: None, - terminal_outcome: None, - }) - } - - /// Append a non-advancing failure that closes one started attempt. - #[allow(clippy::too_many_arguments)] - pub fn append_retryable_failure( - &mut self, - phase: CutoverPhase, - step_id: StepId, - effect_id: EffectId, - effect_kind: EffectKind, - next_phase: Option, - replay_class: ReplayClass, - identity: Option, - destination: Option, - failure_code: FailureCode, - ) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::RetryableFailure, - phase, - step_id: Some(step_id), - effect_id: Some(effect_id), - effect_kind: Some(effect_kind), - next_phase, - replay_class: Some(replay_class), - identity, - destination, - audit_record_id: None, - failure_code: Some(failure_code), - terminal_outcome: None, - }) - } - - /// Append a read-only phase completion record. - pub fn append_phase_completed( - &mut self, - phase: CutoverPhase, - audit_record_id: AuditRecordId, - ) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::PhaseCompleted, - phase, - step_id: None, - effect_id: None, - effect_kind: None, - next_phase: None, - replay_class: None, - identity: None, - destination: None, - audit_record_id: Some(audit_record_id), - failure_code: None, - terminal_outcome: None, - }) - } - - /// Append a hold request. - pub fn append_hold_requested( - &mut self, - phase: CutoverPhase, - audit_record_id: AuditRecordId, - ) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::HoldRequested, - phase, - step_id: None, - effect_id: None, - effect_kind: None, - next_phase: None, - replay_class: None, - identity: None, - destination: None, - audit_record_id: Some(audit_record_id), - failure_code: None, - terminal_outcome: None, - }) - } - - /// Append a hold clear or resume record. - pub fn append_hold_cleared( - &mut self, - phase: CutoverPhase, - audit_record_id: AuditRecordId, - ) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::HoldCleared, - phase, - step_id: None, - effect_id: None, - effect_kind: None, - next_phase: None, - replay_class: None, - identity: None, - destination: None, - audit_record_id: Some(audit_record_id), - failure_code: None, - terminal_outcome: None, - }) - } - - /// Append one write-once terminal record. - pub fn append_terminal( - &mut self, - phase: CutoverPhase, - outcome: TerminalOutcomeKind, - audit_record_id: AuditRecordId, - ) -> Result<(), JournalError> { - self.append(JournalRecordInput { - kind: JournalRecordKind::Terminal, - phase, - step_id: None, - effect_id: None, - effect_kind: None, - next_phase: None, - replay_class: None, - identity: None, - destination: None, - audit_record_id: Some(audit_record_id), - failure_code: None, - terminal_outcome: Some(outcome), - }) - } - - /// Return the most recent started effect without a later completion. - pub fn incomplete_effect(&self) -> Option<&JournalRecord> { - self.records.iter().rev().find(|record| { - record.kind == JournalRecordKind::Started - && record.effect_id.as_ref().is_some_and(|effect_id| { - !self.records.iter().any(|completed| { - matches!( - completed.kind, - JournalRecordKind::Completed | JournalRecordKind::RetryableFailure - ) && completed.effect_id.as_ref() == Some(effect_id) - && completed.sequence > record.sequence - }) - }) - }) - } - - /// Return whether a terminal record has been written. - pub fn is_terminal(&self) -> bool { - self.records - .iter() - .any(|record| record.kind == JournalRecordKind::Terminal) - } - - fn append(&mut self, input: JournalRecordInput) -> Result<(), JournalError> { - if self.is_terminal() { - return Err(JournalError::TerminalAlreadyWritten); - } - if input.kind == JournalRecordKind::Completed - && !self.has_open_effect(input.effect_id.as_ref()) - { - return Err(JournalError::Reordered); - } - if input.kind == JournalRecordKind::RetryableFailure - && !self.has_open_effect(input.effect_id.as_ref()) - { - return Err(JournalError::Reordered); - } - if input.kind == JournalRecordKind::Started - && self.has_completed_effect(input.effect_id.as_ref()) - { - return Err(JournalError::Reordered); - } - if input.kind == JournalRecordKind::Terminal && input.terminal_outcome.is_none() { - return Err(JournalError::Malformed); - } - let sequence = self.records.len() as u64; - let previous_record_digest = self - .records - .last() - .map(|record| record.record_digest.clone()); - let unsigned = JournalRecordUnsigned { - sequence, - operation_id: self.binding.operation_id.clone(), - revision_plan_id: self.binding.revision_plan_id.clone(), - request_digest: self.binding.request_digest.clone(), - previous_record_digest, - kind: input.kind, - phase: input.phase, - step_id: input.step_id, - effect_id: input.effect_id, - effect_kind: input.effect_kind, - next_phase: input.next_phase, - replay_class: input.replay_class, - identity: input.identity, - destination: input.destination, - audit_record_id: input.audit_record_id, - failure_code: input.failure_code, - terminal_outcome: input.terminal_outcome, - }; - let bytes = canonical_json_bytes(&unsigned).map_err(JournalError::CanonicalJson)?; - let record_digest = Digest::parse(canonical_digest(JOURNAL_DOMAIN, &bytes)) - .map_err(|_| JournalError::Digest)?; - self.records.push(JournalRecord { - sequence, - operation_id: unsigned.operation_id, - revision_plan_id: unsigned.revision_plan_id, - request_digest: unsigned.request_digest, - previous_record_digest: unsigned.previous_record_digest, - kind: unsigned.kind, - phase: unsigned.phase, - step_id: unsigned.step_id, - effect_id: unsigned.effect_id, - effect_kind: unsigned.effect_kind, - next_phase: unsigned.next_phase, - replay_class: unsigned.replay_class, - identity: unsigned.identity, - destination: unsigned.destination, - audit_record_id: unsigned.audit_record_id, - failure_code: unsigned.failure_code, - terminal_outcome: unsigned.terminal_outcome, - record_digest, - }); - Ok(()) - } - - fn has_open_effect(&self, effect_id: Option<&EffectId>) -> bool { - let Some(effect_id) = effect_id else { - return false; - }; - let Some(started_sequence) = self - .records - .iter() - .rev() - .find(|record| { - record.kind == JournalRecordKind::Started - && record.effect_id.as_ref() == Some(effect_id) - }) - .map(|record| record.sequence) - else { - return false; - }; - !self.records.iter().any(|record| { - matches!( - record.kind, - JournalRecordKind::Completed | JournalRecordKind::RetryableFailure - ) && record.effect_id.as_ref() == Some(effect_id) - && record.sequence > started_sequence - }) - } - - fn has_completed_effect(&self, effect_id: Option<&EffectId>) -> bool { - let Some(effect_id) = effect_id else { - return false; - }; - self.records.iter().any(|record| { - record.kind == JournalRecordKind::Completed - && record.effect_id.as_ref() == Some(effect_id) - }) - } - - fn validate_record(&self, record: &JournalRecord) -> Result<(), JournalError> { - let expected_sequence = self.records.len() as u64; - if record.sequence != expected_sequence - || record.operation_id != self.binding.operation_id - || record.revision_plan_id != self.binding.revision_plan_id - || record.request_digest != self.binding.request_digest - { - return Err(JournalError::RequestMismatch); - } - let expected_previous = self - .records - .last() - .map(|previous| previous.record_digest.clone()); - if record.previous_record_digest != expected_previous { - return Err(JournalError::Reordered); - } - let bytes = - canonical_json_bytes(&record.unsigned()).map_err(JournalError::CanonicalJson)?; - let expected_digest = Digest::parse(canonical_digest(JOURNAL_DOMAIN, &bytes)) - .map_err(|_| JournalError::Digest)?; - if record.record_digest != expected_digest { - return Err(JournalError::Tampered); - } - if record.kind == JournalRecordKind::Completed - && !self.has_open_effect(record.effect_id.as_ref()) - { - return Err(JournalError::Reordered); - } - if record.kind == JournalRecordKind::RetryableFailure - && !self.has_open_effect(record.effect_id.as_ref()) - { - return Err(JournalError::Reordered); - } - if record.kind == JournalRecordKind::Started - && self.has_completed_effect(record.effect_id.as_ref()) - { - return Err(JournalError::Reordered); - } - if record.kind == JournalRecordKind::Terminal && record.terminal_outcome.is_none() { - return Err(JournalError::Malformed); - } - if self.is_terminal() { - return Err(JournalError::TerminalAlreadyWritten); - } - Ok(()) - } -} - -#[derive(Debug)] -struct JournalRecordInput { - kind: JournalRecordKind, - phase: CutoverPhase, - step_id: Option, - effect_id: Option, - effect_kind: Option, - next_phase: Option, - replay_class: Option, - identity: Option, - destination: Option, - audit_record_id: Option, - failure_code: Option, - terminal_outcome: Option, -} - -/// Journal integrity failures. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum JournalError { - /// Canonical JSON rejected a record. - CanonicalJson(CanonicalJsonError), - /// The line was not terminated by the append-only newline. - Truncated, - /// The JSON shape was not the closed record shape. - Malformed, - /// The line was valid JSON but not canonical bytes. - NonCanonical, - /// A record's request binding did not match. - RequestMismatch, - /// A record's previous digest or sequence was reordered. - Reordered, - /// A record digest was changed. - Tampered, - /// A digest could not be represented canonically. - Digest, - /// A terminal record was followed by another record. - TerminalAlreadyWritten, -} - -impl JournalError { - /// Return the corresponding fail-closed failure class. - pub const fn code(&self) -> FailureCode { - match self { - Self::CanonicalJson(_) | Self::Malformed | Self::NonCanonical | Self::Digest => { - FailureCode::JournalTampered - } - Self::Truncated | Self::RequestMismatch => FailureCode::RequestMismatch, - Self::Reordered => FailureCode::JournalTampered, - Self::Tampered => FailureCode::JournalTampered, - Self::TerminalAlreadyWritten => FailureCode::TerminalAlreadyWritten, - } - } -} - -impl fmt::Display for JournalError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::CanonicalJson(_) => "journal canonical JSON rejected", - Self::Truncated => "journal is truncated", - Self::Malformed => "journal record shape rejected", - Self::NonCanonical => "journal record is not canonical", - Self::RequestMismatch => "journal request binding mismatch", - Self::Reordered => "journal record order mismatch", - Self::Tampered => "journal record digest mismatch", - Self::Digest => "journal digest rejected", - Self::TerminalAlreadyWritten => "journal terminal outcome already written", - }) - } -} - -impl std::error::Error for JournalError {} diff --git a/packages/d2b-cutover/src/lib.rs b/packages/d2b-cutover/src/lib.rs deleted file mode 100644 index 2fc3fd58fb..0000000000 --- a/packages/d2b-cutover/src/lib.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![deny(missing_docs)] - -//! Pure cutover and scoped-reset contracts. - -pub mod consent; -pub mod finalize; -pub mod hold; -pub mod inventory; -pub mod journal; -pub mod model; -pub mod preview; -pub mod reset; -pub mod rollback; -pub mod runner; -pub mod state_machine; -pub mod verify; - -pub use consent::{ - Consent, ConsentBinding, ConsentError, FinalizationBinding, FinalizationConsent, - RecoveryAttestation, -}; -pub use finalize::{FINALIZATION_PHASE, FinalizationError}; -pub use hold::{HoldError, HoldState}; -pub use inventory::{ - HostInventory, InventoryClass, InventoryError, InventoryInputItem, InventoryItem, ZoneInventory, -}; -pub use journal::{ - JOURNAL_DOMAIN, Journal, JournalBinding, JournalError, JournalRecord, JournalRecordKind, -}; -pub use model::{ - ArtifactId, AuditEvidence, AuditRecordId, CandidateId, CompletionEvidence, CutoverPhase, - Digest, Disposition, EffectEvidence, EffectId, EffectKind, EffectOutcome, FailureCode, - HoldReason, HostLockContract, IdError, LockAcquire, LockId, OperationId, OperationKind, - OperationState, OperatorId, RecoveryId, ReplayClass, ReplayDecision, ReplayObservation, - ResetScope, RevisionPlanId, StepId, TerminalOutcomeKind, ZoneId, -}; -pub use preview::{CutoverPreview, PREVIEW_DOMAIN, PreviewError, PreviewInventory}; -pub use reset::{EffectAllowlist, EffectCapability, ResetError, ResetInventory, ResetTarget}; -pub use rollback::{NATIVE_ROLLBACK_BOUNDARY, RollbackError, RollbackResult, plan_native_rollback}; -pub use runner::{ - BootstrapCapability, CapabilityLedger, ConsumedCapability, FinalizationArtifact, - FinalizationPlan, MAX_BOOTSTRAP_LIFETIME_MS, MAX_RUNNER_FRAME_BYTES, RUNNER_BOOTSTRAP_FD, - RUNNER_PROTOCOL_VERSION, RUNNER_SOCKET_TIMEOUT, RunnerBootstrap, RunnerCapabilityError, - RunnerCommand, RunnerLockError, RunnerPaths, RunnerPeer, RunnerResponse, RunnerSocket, - RunnerSocketError, RunnerStatus, RunnerVerificationInput, RunnerZoneVerification, - acquire_operation_lock, load_journal, persist_journal, send_command, write_response, -}; -pub use state_machine::{ - ApplyContext, CutoverEngine, EffectRequest, Operation, OperationError, OperationInventory, - OperationRequest, ReadOnlyEvidence, ResetEngine, -}; -pub use verify::{ - VerificationError, VerificationInput, VerificationReport, ZoneVerification, verify_cutover, -}; diff --git a/packages/d2b-cutover/src/model.rs b/packages/d2b-cutover/src/model.rs deleted file mode 100644 index 5232d1defc..0000000000 --- a/packages/d2b-cutover/src/model.rs +++ /dev/null @@ -1,667 +0,0 @@ -//! Shared closed values for the cutover contract. - -use std::fmt; - -use d2b_contracts_resource::v3::is_canonical_digest; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeserializeError}; - -const MAX_OPAQUE_ID_BYTES: usize = 128; -const MAX_REASON_BYTES: usize = 256; - -macro_rules! opaque_id { - ($name:ident, $label:literal) => { - #[doc = concat!("Opaque ", $label, " identity.")] - #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct $name(String); - - impl $name { - /// Parse one bounded identifier without path syntax. - pub fn new(value: impl Into) -> Result { - let value = value.into(); - if value.is_empty() - || value.len() > MAX_OPAQUE_ID_BYTES - || value - .bytes() - .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace()) - || value.contains('/') - || value.contains('\\') - { - return Err(IdError::Invalid($label)); - } - Ok(Self(value)) - } - - /// Borrow the opaque identifier. - pub fn as_str(&self) -> &str { - &self.0 - } - } - - impl fmt::Debug for $name { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.debug_tuple($label).field(&"").finish() - } - } - - impl fmt::Display for $name { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&self.0) - } - } - - impl Serialize for $name { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.0) - } - } - - impl<'de> Deserialize<'de> for $name { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - Self::new(value).map_err(D::Error::custom) - } - } - }; -} - -opaque_id!(OperationId, "OperationId"); -opaque_id!(CandidateId, "CandidateId"); -opaque_id!(RevisionPlanId, "RevisionPlanId"); -opaque_id!(RecoveryId, "RecoveryId"); -opaque_id!(OperatorId, "OperatorId"); -opaque_id!(ZoneId, "ZoneId"); -opaque_id!(ArtifactId, "ArtifactId"); -opaque_id!(EffectId, "EffectId"); -opaque_id!(StepId, "StepId"); -opaque_id!(LockId, "LockId"); -opaque_id!(AuditRecordId, "AuditRecordId"); - -/// A bounded identity parsing failure. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IdError { - /// The value was empty, oversized, contained control syntax, or looked like a path. - Invalid(&'static str), - /// A digest did not use the canonical SHA-256 spelling. - InvalidDigest, - /// A reason exceeded its bounded text contract. - InvalidReason, -} - -impl fmt::Display for IdError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Invalid(label) => write!(formatter, "invalid {label}"), - Self::InvalidDigest => formatter.write_str("invalid canonical digest"), - Self::InvalidReason => formatter.write_str("invalid bounded reason"), - } - } -} - -impl std::error::Error for IdError {} - -/// A canonical SHA-256 digest used at a contract boundary. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Digest(String); - -impl Digest { - /// Parse a canonical `sha256:` digest. - pub fn parse(value: impl Into) -> Result { - let value = value.into(); - if is_canonical_digest(&value) { - Ok(Self(value)) - } else { - Err(IdError::InvalidDigest) - } - } - - /// Derive a domain-separated digest from canonical bytes. - pub fn derive(domain: &str, bytes: &[u8]) -> Self { - Self(d2b_contracts_resource::v3::canonical_digest(domain, bytes)) - } - - /// Borrow the rendered digest. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl fmt::Debug for Digest { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("Digest()") - } -} - -impl fmt::Display for Digest { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&self.0) - } -} - -impl Serialize for Digest { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.0) - } -} - -impl<'de> Deserialize<'de> for Digest { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Self::parse(String::deserialize(deserializer)?).map_err(D::Error::custom) - } -} - -/// A bounded operator-supplied hold reason. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct HoldReason(String); - -impl HoldReason { - /// Parse a bounded, path-free reason. - pub fn new(value: impl Into) -> Result { - let value = value.into(); - if value.is_empty() - || value.len() > MAX_REASON_BYTES - || value.bytes().any(|byte| byte.is_ascii_control()) - { - return Err(IdError::InvalidReason); - } - Ok(Self(value)) - } - - /// Borrow the reason. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl fmt::Debug for HoldReason { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_tuple("HoldReason") - .field(&"") - .finish() - } -} - -impl Serialize for HoldReason { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.0) - } -} - -impl<'de> Deserialize<'de> for HoldReason { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom) - } -} - -/// The closed phase sequence shared by cutover and reset operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum CutoverPhase { - /// Read-only baseline and candidate checks. - Preflight, - /// Read-only consent admission. - Consent, - /// Read-only authoritative inventory construction. - Inventory, - /// Quiesce the legacy control plane. - Drain, - /// Stage dispositions while sources remain intact. - Disposition, - /// Initialize or reset the resource store. - ResourceStore, - /// Install and start providers. - ProviderInstall, - /// Reconcile Zones and ZoneLinks. - ZoneCutover, - /// Activate Guests and runtime resources. - Activation, - /// Verify every Zone and preserved identity. - Verification, - /// Separately consented old-artifact finalization. - Finalization, -} - -impl CutoverPhase { - /// Return the protocol phase number. - pub const fn number(self) -> u8 { - match self { - Self::Preflight => 0, - Self::Consent => 1, - Self::Inventory => 2, - Self::Drain => 3, - Self::Disposition => 4, - Self::ResourceStore => 5, - Self::ProviderInstall => 6, - Self::ZoneCutover => 7, - Self::Activation => 8, - Self::Verification => 9, - Self::Finalization => 10, - } - } - - /// Return whether native rollback is still available at this phase. - pub const fn is_before_or_at_native_rollback_boundary(self) -> bool { - self.number() <= Self::Disposition.number() - } - - /// Return the next phase, if one exists. - pub const fn next(self) -> Option { - match self { - Self::Preflight => Some(Self::Consent), - Self::Consent => Some(Self::Inventory), - Self::Inventory => Some(Self::Drain), - Self::Drain => Some(Self::Disposition), - Self::Disposition => Some(Self::ResourceStore), - Self::ResourceStore => Some(Self::ProviderInstall), - Self::ProviderInstall => Some(Self::ZoneCutover), - Self::ZoneCutover => Some(Self::Activation), - Self::Activation => Some(Self::Verification), - Self::Verification => Some(Self::Finalization), - Self::Finalization => None, - } - } -} - -/// The distinct operation authorities in this unit. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum OperationKind { - /// One host-wide cutover. - Cutover, - /// One scoped post-cutover reset. - ScopedReset(ResetScope), -} - -impl OperationKind { - /// Return whether this is the host-wide cutover authority. - pub const fn is_cutover(self) -> bool { - matches!(self, Self::Cutover) - } - - /// Return the reset scope, when this is a reset. - pub const fn reset_scope(self) -> Option { - match self { - Self::Cutover => None, - Self::ScopedReset(scope) => Some(scope), - } - } -} - -/// The three supported scoped reset boundaries. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum ResetScope { - /// Reset a complete Zone store. - Zone, - /// Reset one Provider and its owned children. - Provider, - /// Reset one Guest and its owned children. - Guest, -} - -/// The disposition assigned to an inventory item. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum Disposition { - /// Stage a destination while retaining the source. - Adopt, - /// Leave the source untouched. - Preserve, - /// Remove only after a separately cleared finalization gate. - Destroy, -} - -/// Replay behavior for an effect after a crash. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum ReplayClass { - /// The effect may be issued again with the same request. - Repeatable, - /// A journaled identity must be reopened instead of creating again. - ReopenByJournaledIdentity, - /// The effect is unsafe to infer and must quarantine on uncertainty. - QuarantineOnly, -} - -/// Closed effect kinds used by the pure allowlist model. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum EffectKind { - /// Stop the legacy daemon and broker control plane. - HostDrain, - /// Stage one cutover disposition. - CutoverDisposition, - /// Create or reopen the resource store. - ResourceStoreCreate, - /// Install a Provider. - ProviderInstall, - /// Activate the candidate Zone. - ZoneActivation, - /// Activate a Guest or runtime sidecar. - GuestActivation, - /// Verify a Zone or identity-bearing artifact. - Verification, - /// Remove one legacy artifact after phase-10 consent. - CutoverFinalization, - /// Reset one complete Zone. - ScopedZoneReset, - /// Reset one Provider. - ScopedProviderReset, - /// Reset one Guest. - ScopedGuestReset, - /// Destroy a reset-scoped durable Volume when explicitly allowed. - DestroyDurableVolume, - /// Preserve an identity-bearing source. - PreserveSource, - /// Quarantine an ambiguous staged destination. - QuarantineDestination, - /// A cutover-specific broker capability. - CutoverBroker, - /// Activate a frozen system closure. - ClosureActivation, -} - -/// State visible to a pure operation observer. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum OperationState { - /// No apply transition has consumed consent. - Planned, - /// An operator hold is active. - Held, - /// The operation is applying the named phase. - Applying(CutoverPhase), - /// Phase-9 verification succeeded and finalization is pending. - CutoverSucceeded, - /// Phase-10 finalization is active. - Finalizing, - /// Native rollback completed through phase 4. - RolledBack, - /// Recovery from an external snapshot is required. - RestoreRequired, - /// Phase 10 completed and the operation is closed. - Closed, - /// A write-once terminal failure was published. - Failed, -} - -impl OperationState { - /// Return whether this state cannot accept another transition. - pub const fn is_terminal(self) -> bool { - matches!( - self, - Self::RolledBack | Self::RestoreRequired | Self::Closed | Self::Failed - ) - } -} - -/// Closed terminal outcome classes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum TerminalOutcomeKind { - /// Native rollback completed. - RolledBack, - /// The operation crossed phase 5 and needs external restore. - RestoreRequired, - /// Phase 10 finalization completed. - Closed, - /// A fail-closed terminal error was published. - Failed, -} - -/// Stable failure reasons used by transitions and replay. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum FailureCode { - /// The host inventory did not cover every configured Zone. - InventoryIncomplete, - /// The inventory contained inconsistent or duplicate state. - InventoryInconsistent, - /// The caller attempted to enumerate gateway credentials or audit. - GatewayCredentialAuditEnumerationForbidden, - /// Consent did not match the operation or was already consumed. - ConsentInvalid, - /// Recovery evidence did not match the bound candidate. - RecoveryMismatch, - /// Candidate, marker, or ownership evidence drifted. - CandidateDrift, - /// A request or journal digest did not match. - RequestMismatch, - /// Journal integrity could not be proven. - JournalTampered, - /// Another operation owns the host-wide linearization lock. - LockContended, - /// A hold blocks the next destructive transition. - HoldActive, - /// The effect failed before durable completion. - EffectFailed, - /// Audit evidence was not durable. - AuditNotDurable, - /// An identity-bearing replay could not prove the original identity. - IdentityMismatch, - /// A staged destination was partial or ambiguous. - DestinationAmbiguous, - /// Native rollback was requested after phase 4. - RollbackWindowClosed, - /// The source artifact was not retained as required. - SourceNotPreserved, - /// The operation attempted a forbidden effect. - EffectNotAllowed, - /// A phase was not ready for the requested transition. - InvalidTransition, - /// A terminal result already exists. - TerminalAlreadyWritten, - /// Finalization consent was missing or mismatched. - FinalizationConsentRequired, - /// Verification did not cover every required Zone. - VerificationIncomplete, -} - -/// A durable effect result supplied by an external adapter. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EffectOutcome { - /// The effect completed. - Succeeded, - /// The effect failed before completion. - Failed, - /// The effect may have completed but ownership is ambiguous. - Ambiguous, -} - -/// A pure observation of the privileged audit publication boundary. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AuditEvidence { - durable: bool, - record_id: Option, -} - -impl AuditEvidence { - /// Construct durable audit evidence. - pub fn durable(record_id: impl Into) -> Result { - Ok(Self { - durable: true, - record_id: Some(AuditRecordId::new(record_id)?), - }) - } - - /// Construct a failed audit publication result. - pub const fn unavailable() -> Self { - Self { - durable: false, - record_id: None, - } - } - - /// Return whether the audit record is durable. - pub const fn is_durable(&self) -> bool { - self.durable - } - - /// Return the opaque audit record identity. - pub fn record_id(&self) -> Option<&AuditRecordId> { - self.record_id.as_ref() - } -} - -/// A pure effect result, optionally carrying a journaled identity. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EffectEvidence { - outcome: EffectOutcome, - identity: Option, -} - -impl EffectEvidence { - /// Construct a successful effect without an identity-bearing result. - pub const fn succeeded() -> Self { - Self { - outcome: EffectOutcome::Succeeded, - identity: None, - } - } - - /// Construct a successful effect with an observed identity. - pub fn succeeded_with_identity(identity: impl Into) -> Result { - Ok(Self { - outcome: EffectOutcome::Succeeded, - identity: Some(ArtifactId::new(identity)?), - }) - } - - /// Construct a failed effect. - pub const fn failed() -> Self { - Self { - outcome: EffectOutcome::Failed, - identity: None, - } - } - - /// Construct an ambiguous effect. - pub const fn ambiguous() -> Self { - Self { - outcome: EffectOutcome::Ambiguous, - identity: None, - } - } - - /// Return the effect outcome. - pub const fn outcome(&self) -> &EffectOutcome { - &self.outcome - } - - /// Return the observed identity, if one was supplied. - pub fn identity(&self) -> Option<&ArtifactId> { - self.identity.as_ref() - } -} - -/// Evidence required before a mutating transition may advance. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CompletionEvidence { - /// Effect result from the injected adapter. - pub effect: EffectEvidence, - /// Durable privileged audit result. - pub audit: AuditEvidence, -} - -/// Observation used to classify a crash-replay boundary. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ReplayObservation { - /// No durable destination was observed. - Absent, - /// The expected journaled identity is present and valid. - JournaledIdentity(ArtifactId), - /// A different identity replaced the expected destination. - WrongIdentity, - /// More than one identity claims the destination. - DuplicateIdentity, - /// The provisioning marker is invalid or missing. - InvalidMarker, - /// A destination exists but is incomplete. - PartialDestination, - /// A previously accepted destination was replaced. - ReplacedDestination, - /// A foreign owner controls the destination. - ForeignOwner, - /// The observation cannot be classified safely. - Ambiguous, -} - -/// The pure replay instruction returned for one effect. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ReplayDecision { - /// Re-run the repeatable effect. - Repeat, - /// Reopen the original identity without creating another one. - Reopen(ArtifactId), - /// Stop and quarantine the ambiguous destination. - Quarantine(FailureCode), -} - -/// A bounded host-wide linearization lock model. -#[derive(Debug, Default)] -pub struct HostLockContract { - owner: Option, -} - -/// Result of attempting to acquire the host-wide lock. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum LockAcquire { - /// The caller now owns the lock. - Acquired, - /// Another operation already owns the lock. - Contended(OperationId), - /// The caller already owns the lock. - AlreadyOwned, -} - -impl HostLockContract { - /// Construct an unowned host-wide lock model. - pub const fn new() -> Self { - Self { owner: None } - } - - /// Attempt one linearized acquisition without touching the host. - pub fn acquire(&mut self, operation: &OperationId) -> LockAcquire { - match &self.owner { - None => { - self.owner = Some(operation.clone()); - LockAcquire::Acquired - } - Some(owner) if owner == operation => LockAcquire::AlreadyOwned, - Some(owner) => LockAcquire::Contended(owner.clone()), - } - } - - /// Release the lock only for its current owner. - pub fn release(&mut self, operation: &OperationId) -> bool { - if self.owner.as_ref() == Some(operation) { - self.owner = None; - true - } else { - false - } - } - - /// Return the opaque current owner. - pub fn owner(&self) -> Option<&OperationId> { - self.owner.as_ref() - } -} diff --git a/packages/d2b-cutover/src/preview.rs b/packages/d2b-cutover/src/preview.rs deleted file mode 100644 index affde77959..0000000000 --- a/packages/d2b-cutover/src/preview.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! Canonical, non-mutating preview construction. - -use std::fmt; - -use d2b_contracts_resource::v3::{ - CanonicalJsonError, CanonicalJsonValue, canonical_digest, canonical_json_bytes, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - inventory::{HostInventory, InventoryError}, - model::{ - ArtifactId, CandidateId, CutoverPhase, Digest, OperationId, OperationKind, RevisionPlanId, - }, - reset::{EffectAllowlist, ResetInventory}, -}; - -/// Domain separator for cutover preview digests. -pub const PREVIEW_DOMAIN: &str = "d2b:cutover:preview:v1"; - -/// Inventory carried by a canonical preview. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", tag = "kind", content = "value")] -pub enum PreviewInventory { - /// Host-wide all-Zone inventory. - Host(HostInventory), - /// One scoped reset inventory. - Reset(ResetInventory), -} - -/// A canonical dry-run preview bound to one operation and inventory. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CutoverPreview { - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - inventory: PreviewInventory, - #[serde(default, skip_serializing_if = "Option::is_none")] - system_artifact_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_system_artifact_id: Option, - recovery_digest: Option, - rollback_boundary: CutoverPhase, - effect_allowlist: EffectAllowlist, -} - -impl CutoverPreview { - /// Build a preview from a normalized inventory without mutating the host. - pub fn new( - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - inventory: HostInventory, - recovery_digest: Option, - ) -> Result { - if !operation_kind.is_cutover() { - return Err(PreviewError::Inventory( - InventoryError::InventoryKindMismatch, - )); - } - if operation_kind.is_cutover() && inventory.zones().is_empty() { - return Err(PreviewError::Inventory(InventoryError::NoConfiguredZones)); - } - Ok(Self { - operation_id, - operation_kind, - candidate_id, - revision_plan_id, - inventory: PreviewInventory::Host(inventory), - system_artifact_id: None, - source_system_artifact_id: None, - recovery_digest, - rollback_boundary: CutoverPhase::Disposition, - effect_allowlist: EffectAllowlist::for_operation(operation_kind), - }) - } - - /// Build a preview for a scoped reset without widening its inventory. - pub fn new_reset( - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - inventory: ResetInventory, - ) -> Result { - if operation_kind.reset_scope() != Some(inventory.scope()) { - return Err(PreviewError::Inventory( - InventoryError::InventoryKindMismatch, - )); - } - Ok(Self { - operation_id, - operation_kind, - candidate_id, - revision_plan_id, - inventory: PreviewInventory::Reset(inventory), - system_artifact_id: None, - source_system_artifact_id: None, - recovery_digest: None, - rollback_boundary: CutoverPhase::Disposition, - effect_allowlist: EffectAllowlist::for_operation(operation_kind), - }) - } - - /// Borrow the operation identity. - pub fn operation_id(&self) -> &OperationId { - &self.operation_id - } - - /// Return the operation kind. - pub const fn operation_kind(&self) -> OperationKind { - self.operation_kind - } - - /// Borrow the candidate identity. - pub fn candidate_id(&self) -> &CandidateId { - &self.candidate_id - } - - /// Borrow the revision-bound plan identity. - pub fn revision_plan_id(&self) -> &RevisionPlanId { - &self.revision_plan_id - } - - /// Borrow the all-Zone inventory. - pub fn inventory(&self) -> &PreviewInventory { - &self.inventory - } - - /// Borrow the optional system artifact identity bound to a cutover. - pub fn system_artifact_id(&self) -> Option<&ArtifactId> { - self.system_artifact_id.as_ref() - } - - /// Bind the exact system artifact selected by the frozen candidate. - pub fn with_system_artifact_id(mut self, system_artifact_id: ArtifactId) -> Self { - self.system_artifact_id = Some(system_artifact_id); - self - } - - /// Borrow the preserved source artifact identity bound to native rollback. - pub fn source_system_artifact_id(&self) -> Option<&ArtifactId> { - self.source_system_artifact_id.as_ref() - } - - /// Bind the exact preserved source artifact selected for native rollback. - pub fn with_source_system_artifact_id(mut self, source_system_artifact_id: ArtifactId) -> Self { - self.source_system_artifact_id = Some(source_system_artifact_id); - self - } - - /// Borrow the host inventory when this is a cutover preview. - pub fn host_inventory(&self) -> Option<&HostInventory> { - match &self.inventory { - PreviewInventory::Host(inventory) => Some(inventory), - PreviewInventory::Reset(_) => None, - } - } - - /// Borrow the reset inventory when this is a reset preview. - pub fn reset_inventory(&self) -> Option<&ResetInventory> { - match &self.inventory { - PreviewInventory::Host(_) => None, - PreviewInventory::Reset(inventory) => Some(inventory), - } - } - - /// Return the recovery digest bound into this preview. - pub fn recovery_digest(&self) -> Option<&Digest> { - self.recovery_digest.as_ref() - } - - /// Return the explicit native rollback boundary. - pub const fn rollback_boundary(&self) -> CutoverPhase { - self.rollback_boundary - } - - /// Borrow the closed effect allowlist. - pub fn effect_allowlist(&self) -> &EffectAllowlist { - &self.effect_allowlist - } - - /// Render exact canonical preview bytes. - pub fn canonical_bytes(&self) -> Result, PreviewError> { - canonical_json_bytes(self).map_err(PreviewError::CanonicalJson) - } - - /// Compute the domain-separated preview digest. - pub fn digest(&self) -> Result { - let bytes = self.canonical_bytes()?; - Ok(Digest::parse(canonical_digest(PREVIEW_DOMAIN, &bytes))?) - } - - /// Decode a preview through the strict canonical JSON path. - pub fn decode_json(bytes: &[u8]) -> Result { - CanonicalJsonValue::parse(bytes).map_err(PreviewError::CanonicalJson)?; - let preview: Self = - serde_json::from_slice(bytes).map_err(|_| PreviewError::MalformedJson)?; - preview.validate()?; - Ok(preview) - } - - fn validate(&self) -> Result<(), PreviewError> { - match (&self.operation_kind, &self.inventory) { - (OperationKind::Cutover, PreviewInventory::Host(inventory)) - if !inventory.zones().is_empty() => - { - inventory.validate().map_err(PreviewError::Inventory)?; - } - (OperationKind::ScopedReset(scope), PreviewInventory::Reset(inventory)) - if Some(*scope) == Some(inventory.scope()) => {} - (OperationKind::Cutover, _) | (OperationKind::ScopedReset(_), _) => { - return Err(PreviewError::Inventory( - InventoryError::InventoryKindMismatch, - )); - } - } - if self.rollback_boundary != CutoverPhase::Disposition - || self.effect_allowlist != EffectAllowlist::for_operation(self.operation_kind) - { - return Err(PreviewError::InvalidContract); - } - Ok(()) - } -} - -/// Preview construction or canonicalization failure. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreviewError { - /// The inventory was incomplete or inconsistent. - Inventory(InventoryError), - /// Canonical JSON rejected a value. - CanonicalJson(CanonicalJsonError), - /// A digest could not be parsed. - Digest(crate::model::IdError), - /// The typed preview JSON shape was invalid. - MalformedJson, - /// A decoded preview violated its closed contract. - InvalidContract, -} - -impl fmt::Display for PreviewError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::Inventory(_) => "preview inventory rejected", - Self::CanonicalJson(_) => "preview canonicalization failed", - Self::Digest(_) => "preview digest failed", - Self::MalformedJson => "preview JSON shape rejected", - Self::InvalidContract => "preview contract rejected", - }) - } -} - -impl std::error::Error for PreviewError {} - -impl From for PreviewError { - fn from(error: InventoryError) -> Self { - Self::Inventory(error) - } -} - -impl From for PreviewError { - fn from(error: crate::model::IdError) -> Self { - Self::Digest(error) - } -} diff --git a/packages/d2b-cutover/src/reset.rs b/packages/d2b-cutover/src/reset.rs deleted file mode 100644 index ca20e056ac..0000000000 --- a/packages/d2b-cutover/src/reset.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Scoped reset inventories and operation-specific effect capabilities. - -use std::collections::BTreeSet; -use std::fmt; - -use d2b_contracts_resource::v3::{CanonicalJsonError, canonical_digest, canonical_json_bytes}; -use serde::{Deserialize, Serialize}; - -use crate::model::{ - ArtifactId, Digest, EffectKind, FailureCode, OperationId, OperationKind, OperatorId, ResetScope, -}; - -/// A scope-specific reset target. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ResetTarget { - scope: ResetScope, - identity: ArtifactId, -} - -impl ResetTarget { - /// Construct a target for one reset scope. - pub fn new(scope: ResetScope, identity: impl Into) -> Result { - Ok(Self { - scope, - identity: ArtifactId::new(identity).map_err(|_| ResetError::InvalidTarget)?, - }) - } - - /// Return the target scope. - pub const fn scope(&self) -> ResetScope { - self.scope - } - - /// Borrow the opaque target identity. - pub fn identity(&self) -> &ArtifactId { - &self.identity - } -} - -/// A reset inventory that cannot widen into host-wide cutover state. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ResetInventory { - scope: ResetScope, - target: ResetTarget, - preserve_durable_volumes: bool, - destroy_durable_consent: bool, -} - -impl ResetInventory { - /// Build the default preserve-durable reset inventory. - pub fn new(scope: ResetScope, identity: impl Into) -> Result { - let target = ResetTarget::new(scope, identity)?; - Ok(Self { - scope, - target, - preserve_durable_volumes: true, - destroy_durable_consent: false, - }) - } - - /// Set whether durable Volumes remain detached and preserved. - pub const fn with_preserve_durable_volumes(mut self, preserve: bool) -> Self { - self.preserve_durable_volumes = preserve; - self - } - - /// Bind the separate consent required to destroy durable Volumes. - pub const fn with_destroy_durable_consent(mut self, consented: bool) -> Self { - self.destroy_durable_consent = consented; - self - } - - /// Return the reset scope. - pub const fn scope(&self) -> ResetScope { - self.scope - } - - /// Borrow the single reset target. - pub fn target(&self) -> &ResetTarget { - &self.target - } - - /// Return whether durable Volumes are preserved by default. - pub const fn preserves_durable_volumes(&self) -> bool { - self.preserve_durable_volumes - } - - /// Return whether this inventory separately authorizes durable destruction. - pub const fn allows_destroy_durable_volumes(&self) -> bool { - !self.preserve_durable_volumes && self.destroy_durable_consent - } - - /// Render exact canonical reset-inventory bytes. - pub fn canonical_bytes(&self) -> Result, ResetError> { - canonical_json_bytes(self).map_err(ResetError::CanonicalJson) - } - - /// Compute the digest bound to a reset request. - pub fn digest(&self) -> Result { - Digest::parse(canonical_digest( - "d2b:cutover:reset-inventory:v1", - &self.canonical_bytes()?, - )) - .map_err(|_| ResetError::Digest) - } -} - -/// Closed operation-specific effect allowlist. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct EffectAllowlist { - operation_kind: OperationKind, - effects: BTreeSet, -} - -impl EffectAllowlist { - /// Construct the closed allowlist for an operation kind. - pub fn for_operation(operation_kind: OperationKind) -> Self { - let effects = match operation_kind { - OperationKind::Cutover => BTreeSet::from([ - EffectKind::HostDrain, - EffectKind::CutoverDisposition, - EffectKind::ResourceStoreCreate, - EffectKind::ProviderInstall, - EffectKind::ZoneActivation, - EffectKind::GuestActivation, - EffectKind::Verification, - EffectKind::CutoverFinalization, - EffectKind::PreserveSource, - EffectKind::QuarantineDestination, - EffectKind::CutoverBroker, - EffectKind::ClosureActivation, - ]), - OperationKind::ScopedReset(scope) => { - let reset_effect = match scope { - ResetScope::Zone => EffectKind::ScopedZoneReset, - ResetScope::Provider => EffectKind::ScopedProviderReset, - ResetScope::Guest => EffectKind::ScopedGuestReset, - }; - BTreeSet::from([ - reset_effect, - EffectKind::DestroyDurableVolume, - EffectKind::PreserveSource, - EffectKind::QuarantineDestination, - EffectKind::Verification, - ]) - } - }; - Self { - operation_kind, - effects, - } - } - - /// Return the operation kind bound to this allowlist. - pub const fn operation_kind(&self) -> OperationKind { - self.operation_kind - } - - /// Return whether one effect is authorized. - pub fn permits(&self, effect: EffectKind) -> bool { - self.effects.contains(&effect) - } - - /// Return all effects in canonical order. - pub fn effects(&self) -> impl Iterator { - self.effects.iter() - } - - /// Reject effects that belong to a different authority. - pub fn require(&self, effect: EffectKind) -> Result<(), ResetError> { - if self.permits(effect) { - Ok(()) - } else { - Err(ResetError::EffectNotAllowed(effect)) - } - } -} - -impl OperationKind { - /// Return the closed effect allowlist for this operation authority. - pub fn allowlist(self) -> EffectAllowlist { - EffectAllowlist::for_operation(self) - } -} - -/// A pure capability passed to one operation-scoped effect adapter. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EffectCapability { - operation_id: OperationId, - operator_id: OperatorId, - allowlist: EffectAllowlist, -} - -impl EffectCapability { - /// Bind one capability to an operation and operator. - pub fn new( - operation_id: OperationId, - operator_id: OperatorId, - operation_kind: OperationKind, - ) -> Self { - Self { - operation_id, - operator_id, - allowlist: EffectAllowlist::for_operation(operation_kind), - } - } - - /// Borrow the operation identity. - pub fn operation_id(&self) -> &OperationId { - &self.operation_id - } - - /// Borrow the operator identity. - pub fn operator_id(&self) -> &OperatorId { - &self.operator_id - } - - /// Return the operation kind. - pub const fn operation_kind(&self) -> OperationKind { - self.allowlist.operation_kind() - } - - /// Validate one effect without performing it. - pub fn authorize(&self, effect: EffectKind) -> Result<(), ResetError> { - self.allowlist.require(effect) - } -} - -/// Scoped reset failures. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ResetError { - /// The target identity was invalid. - InvalidTarget, - /// An effect belongs to another operation authority. - EffectNotAllowed(EffectKind), - /// A reset inventory was used for a different scope. - ScopeMismatch, - /// Canonical reset inventory encoding failed. - CanonicalJson(CanonicalJsonError), - /// Reset inventory digest encoding failed. - Digest, -} - -impl ResetError { - /// Return the stable failure class. - pub const fn code(&self) -> FailureCode { - match self { - Self::InvalidTarget => FailureCode::InventoryInconsistent, - Self::EffectNotAllowed(_) | Self::ScopeMismatch => FailureCode::EffectNotAllowed, - Self::CanonicalJson(_) | Self::Digest => FailureCode::InventoryInconsistent, - } - } -} - -impl fmt::Display for ResetError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::InvalidTarget => "invalid reset target", - Self::EffectNotAllowed(_) => "effect is not allowed for reset scope", - Self::ScopeMismatch => "reset scope mismatch", - Self::CanonicalJson(_) => "reset inventory canonicalization failed", - Self::Digest => "reset inventory digest failed", - }) - } -} - -impl std::error::Error for ResetError {} diff --git a/packages/d2b-cutover/src/rollback.rs b/packages/d2b-cutover/src/rollback.rs deleted file mode 100644 index 68f90cb676..0000000000 --- a/packages/d2b-cutover/src/rollback.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Native rollback boundary and source-preservation contract. - -use std::fmt; - -use crate::{ - inventory::HostInventory, - model::{ArtifactId, CutoverPhase, FailureCode}, -}; - -/// The last phase covered by native rollback. -pub const NATIVE_ROLLBACK_BOUNDARY: CutoverPhase = CutoverPhase::Disposition; - -/// Result of a pure native rollback decision. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RollbackResult { - phase: CutoverPhase, - sources_preserved: bool, - quarantined_destinations: Vec, -} - -impl RollbackResult { - /// Return the phase at which rollback was requested. - pub const fn phase(&self) -> CutoverPhase { - self.phase - } - - /// Return whether every identity-bearing source remained intact. - pub const fn sources_preserved(&self) -> bool { - self.sources_preserved - } - - /// Borrow staged destinations that must be quarantined. - pub fn quarantined_destinations(&self) -> &[ArtifactId] { - &self.quarantined_destinations - } -} - -/// Check the rollback boundary and derive source-preservation obligations. -pub fn plan_native_rollback( - phase: CutoverPhase, - inventory: &HostInventory, - staged_destinations: impl IntoIterator, -) -> Result { - if !phase.is_before_or_at_native_rollback_boundary() { - return Err(RollbackError::BoundaryClosed(phase)); - } - if !inventory.sources_retained() { - return Err(RollbackError::SourcesNotPreserved); - } - let mut quarantined_destinations = staged_destinations.into_iter().collect::>(); - quarantined_destinations.sort(); - quarantined_destinations.dedup(); - Ok(RollbackResult { - phase, - sources_preserved: true, - quarantined_destinations, - }) -} - -/// Rollback failures. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RollbackError { - /// The operation crossed the native rollback boundary. - BoundaryClosed(CutoverPhase), - /// An identity-bearing source was not retained. - SourcesNotPreserved, -} - -impl RollbackError { - /// Return the stable failure class. - pub const fn code(&self) -> FailureCode { - match self { - Self::BoundaryClosed(_) => FailureCode::RollbackWindowClosed, - Self::SourcesNotPreserved => FailureCode::SourceNotPreserved, - } - } -} - -impl fmt::Display for RollbackError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::BoundaryClosed(_) => "native rollback boundary is closed", - Self::SourcesNotPreserved => "rollback source-preservation invariant failed", - }) - } -} - -impl std::error::Error for RollbackError {} diff --git a/packages/d2b-cutover/src/runner.rs b/packages/d2b-cutover/src/runner.rs deleted file mode 100644 index cd98c27776..0000000000 --- a/packages/d2b-cutover/src/runner.rs +++ /dev/null @@ -1,1241 +0,0 @@ -//! Out-of-band cutover runner capability and lifecycle primitives. -//! -//! This module owns only the runner boundary. It does not perform a host -//! mutation and it never exposes the durable journal to a client. The -//! broker-created child consumes [`BootstrapCapability`] once, then uses the -//! owner-authenticated socket for status and safety holds while the daemon is -//! unavailable. - -use std::{ - collections::BTreeSet, - fs::{self, File, OpenOptions}, - io::{self, Read, Write}, - os::{ - fd::{AsRawFd, RawFd}, - unix::{ - fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt}, - net::{UnixListener, UnixStream}, - }, - }, - path::{Path, PathBuf}, - time::Duration, -}; - -use d2b_contracts_resource::v3::{CanonicalJsonValue, canonical_json_bytes}; -use nix::{ - fcntl::{FcntlArg, fcntl}, - libc, - sys::socket::{getsockopt, sockopt::PeerCredentials}, - unistd::{Gid, Uid, chown, geteuid}, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - ArtifactId, CandidateId, Consent, CutoverPhase, CutoverPreview, Digest, EffectAllowlist, - EffectId, EffectKind, Journal, JournalBinding, Operation, OperationId, OperationKind, - OperationRequest, OperationState, OperatorId, RecoveryAttestation, ReplayClass, StepId, ZoneId, -}; - -/// Protocol version for the runner bootstrap and owner socket. -pub const RUNNER_PROTOCOL_VERSION: u16 = 1; -/// Maximum bytes accepted from a broker bootstrap fd or owner socket frame. -pub const MAX_RUNNER_FRAME_BYTES: usize = 64 * 1024; -/// Maximum lifetime of a bootstrap capability. -pub const MAX_BOOTSTRAP_LIFETIME_MS: u64 = 15 * 60 * 1000; -/// Fixed fd number used by the runner child for its one-shot bootstrap. -pub const RUNNER_BOOTSTRAP_FD: RawFd = 3; -/// Maximum duration for one owner-socket round trip. -pub const RUNNER_SOCKET_TIMEOUT: Duration = Duration::from_secs(10); - -/// A single-use capability transferred from the live broker to the runner. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct BootstrapCapability { - version: u16, - operation_id: OperationId, - candidate_id: CandidateId, - operator_id: OperatorId, - operation_kind: OperationKind, - effect_allowlist: EffectAllowlist, - nonce: Digest, - issued_at_ms: u64, - expires_at_ms: u64, - /// Uid of the bound operator, used only for socket authentication. - operator_uid: u32, - /// Admin uids admitted to set a safety hold during drain. - admin_uids: BTreeSet, - /// Optional lifecycle group gid used for the socket filesystem gate. - #[serde(default, skip_serializing_if = "Option::is_none")] - lifecycle_gid: Option, -} - -impl BootstrapCapability { - /// Construct a bounded capability for one operation. - #[allow(clippy::too_many_arguments)] - pub fn new( - operation_id: OperationId, - candidate_id: CandidateId, - operator_id: OperatorId, - operation_kind: OperationKind, - nonce: Digest, - issued_at_ms: u64, - expires_at_ms: u64, - ) -> Result { - Self::new_with_identity( - operation_id, - candidate_id, - operator_id, - operation_kind, - nonce, - issued_at_ms, - expires_at_ms, - 0, - BTreeSet::new(), - ) - } - - /// Construct a capability with the socket identity allowlist. - #[allow(clippy::too_many_arguments)] - pub fn new_with_identity( - operation_id: OperationId, - candidate_id: CandidateId, - operator_id: OperatorId, - operation_kind: OperationKind, - nonce: Digest, - issued_at_ms: u64, - expires_at_ms: u64, - operator_uid: u32, - admin_uids: BTreeSet, - ) -> Result { - validate_time_window(issued_at_ms, expires_at_ms)?; - Self::new_with_identity_and_group( - operation_id, - candidate_id, - operator_id, - operation_kind, - nonce, - issued_at_ms, - expires_at_ms, - operator_uid, - admin_uids, - None, - ) - } - - /// Construct a capability with identity and lifecycle group bindings. - #[allow(clippy::too_many_arguments)] - pub fn new_with_identity_and_group( - operation_id: OperationId, - candidate_id: CandidateId, - operator_id: OperatorId, - operation_kind: OperationKind, - nonce: Digest, - issued_at_ms: u64, - expires_at_ms: u64, - operator_uid: u32, - admin_uids: BTreeSet, - lifecycle_gid: Option, - ) -> Result { - validate_time_window(issued_at_ms, expires_at_ms)?; - Ok(Self { - version: RUNNER_PROTOCOL_VERSION, - operation_id, - candidate_id, - operator_id, - operation_kind, - effect_allowlist: EffectAllowlist::for_operation(operation_kind), - nonce, - issued_at_ms, - expires_at_ms, - operator_uid, - admin_uids, - lifecycle_gid, - }) - } - - /// Render the exact canonical capability bytes. - pub fn canonical_bytes(&self) -> Result, RunnerCapabilityError> { - canonical_json_bytes(self).map_err(|_| RunnerCapabilityError::CanonicalJson) - } - - /// Derive the capability proof presented to an adapted broker. - pub fn binding_digest(&self) -> Digest { - let bytes = self - .canonical_bytes() - .expect("validated capability serializes"); - Digest::derive("d2b:cutover:runner-capability:v1", &bytes) - } - - /// Decode and consume one capability from canonical bytes. - pub fn decode_and_consume( - bytes: &[u8], - now_ms: u64, - ledger: &mut CapabilityLedger, - ) -> Result { - CanonicalJsonValue::parse(bytes).map_err(|_| RunnerCapabilityError::CanonicalJson)?; - let capability: Self = - serde_json::from_slice(bytes).map_err(|_| RunnerCapabilityError::Malformed)?; - capability.consume(now_ms, ledger) - } - - /// Borrow the operation identity. - pub fn operation_id(&self) -> &OperationId { - &self.operation_id - } - - /// Borrow the candidate identity. - pub fn candidate_id(&self) -> &CandidateId { - &self.candidate_id - } - - /// Borrow the bound operator identity. - pub fn operator_id(&self) -> &OperatorId { - &self.operator_id - } - - /// Return the operation authority. - pub const fn operation_kind(&self) -> OperationKind { - self.operation_kind - } - - /// Borrow the closed effect allowlist. - pub fn effect_allowlist(&self) -> &EffectAllowlist { - &self.effect_allowlist - } - - /// Return the bound operator uid. - pub const fn operator_uid(&self) -> u32 { - self.operator_uid - } - - /// Return whether a uid is one of the admitted Admin peers. - pub fn is_admin_uid(&self, uid: u32) -> bool { - self.admin_uids.contains(&uid) - } - - /// Return the optional lifecycle group gid. - pub const fn lifecycle_gid(&self) -> Option { - self.lifecycle_gid - } - - /// Return the capability expiry. - pub const fn expires_at_ms(&self) -> u64 { - self.expires_at_ms - } - - fn consume( - self, - now_ms: u64, - ledger: &mut CapabilityLedger, - ) -> Result { - if self.version != RUNNER_PROTOCOL_VERSION { - return Err(RunnerCapabilityError::Version); - } - validate_time_window(self.issued_at_ms, self.expires_at_ms)?; - if now_ms < self.issued_at_ms || now_ms > self.expires_at_ms { - return Err(RunnerCapabilityError::Expired); - } - if self.effect_allowlist != EffectAllowlist::for_operation(self.operation_kind) { - return Err(RunnerCapabilityError::EffectAllowlistMismatch); - } - if !ledger.nonces.insert(self.nonce.clone()) { - return Err(RunnerCapabilityError::AlreadyConsumed); - } - Ok(ConsumedCapability(self)) - } -} - -/// An in-process replay ledger for bootstrap capabilities. -#[derive(Debug, Default)] -pub struct CapabilityLedger { - nonces: BTreeSet, -} - -/// The capability after its one-time consumption boundary. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConsumedCapability(BootstrapCapability); - -impl ConsumedCapability { - /// Return the operation authority. - pub const fn operation_kind(&self) -> OperationKind { - self.0.operation_kind - } - - /// Borrow the underlying operation identity. - pub fn operation_id(&self) -> &OperationId { - &self.0.operation_id - } - - /// Borrow the candidate identity. - pub fn candidate_id(&self) -> &CandidateId { - &self.0.candidate_id - } - - /// Borrow the bound operator identity. - pub fn operator_id(&self) -> &OperatorId { - &self.0.operator_id - } - - /// Return whether this capability admits a peer uid as an Admin. - pub fn is_admin_uid(&self, uid: u32) -> bool { - self.0.is_admin_uid(uid) - } - - /// Return the optional lifecycle group gid. - pub const fn lifecycle_gid(&self) -> Option { - self.0.lifecycle_gid - } - - /// Return the bound operator uid. - pub const fn operator_uid(&self) -> u32 { - self.0.operator_uid - } - - /// Borrow the closed effect allowlist. - pub fn effect_allowlist(&self) -> &EffectAllowlist { - &self.0.effect_allowlist - } - - /// Derive the capability binding digest for restart adoption checks. - pub fn binding_digest(&self) -> Digest { - self.0.binding_digest() - } - - /// Return the expiry. - pub const fn expires_at_ms(&self) -> u64 { - self.0.expires_at_ms - } -} - -/// The complete bootstrap payload sent over the single-use fd. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RunnerBootstrap { - /// Capability consumed before any runner state is opened. - pub capability: BootstrapCapability, - /// Immutable operation request bound to the preview. - pub request: OperationRequest, - /// Canonical preview used to reconstruct the pure engine. - pub preview: CutoverPreview, - /// Exact single-use apply consent, when this runner is admitted to apply. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub consent: Option, - /// Separate destructive consent for durable-Volume reset effects. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub destructive_consent: Option, - /// Qualified external recovery evidence for cutover apply. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub recovery: Option, - /// Host identity digest bound by the recovery evidence. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_digest: Option, -} - -/// One approved legacy artifact disposition for phase-10 finalization. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct FinalizationArtifact { - /// Opaque artifact identity. - pub artifact_id: ArtifactId, - /// Digest of the disposition approved by the operation inventory. - pub disposition_digest: Digest, -} - -/// Phase-10 finalization payload bound to one operation. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct FinalizationPlan { - /// Approved artifact dispositions. - pub artifacts: Vec, -} - -/// One redaction-safe post-activation Zone observation. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RunnerZoneVerification { - /// Opaque Zone identity. - pub zone_id: ZoneId, - /// Whether the Zone passed its authoritative checks. - pub healthy: bool, -} - -/// Authoritative observations required by phase-9 verification. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RunnerVerificationInput { - /// All configured Zone observations. - pub zones: Vec, - /// Whether every preserved source remained intact. - pub sources_preserved: bool, - /// Whether identity digests matched the cutover snapshot. - pub identity_digests_match: bool, - /// Whether the candidate remains current. - pub candidate_current: bool, -} - -impl RunnerBootstrap { - /// Validate the bootstrap's cross-object identity bindings. - pub fn validate(&self) -> Result<(), RunnerCapabilityError> { - let capability = &self.capability; - if capability.operation_id() != self.request.operation_id() - || capability.candidate_id() != self.request.candidate_id() - || capability.operator_id() != self.request.operator_id() - || capability.operation_kind() != self.request.operation_kind() - { - return Err(RunnerCapabilityError::IdentityMismatch); - } - if self.preview.operation_id() != self.request.operation_id() - || self.preview.candidate_id() != self.request.candidate_id() - || self.preview.revision_plan_id() != self.request.revision_plan_id() - || self.preview.operation_kind() != self.request.operation_kind() - || self - .preview - .digest() - .map_err(|_| RunnerCapabilityError::Preview)? - != *self.request.preview_digest() - { - return Err(RunnerCapabilityError::Preview); - } - if !self - .request - .request_digest_matches() - .map_err(|_| RunnerCapabilityError::Request)? - { - return Err(RunnerCapabilityError::Request); - } - if self.request.system_artifact_id() != self.preview.system_artifact_id() - || self.request.source_system_artifact_id() != self.preview.source_system_artifact_id() - { - return Err(RunnerCapabilityError::Preview); - } - Operation::new(self.request.clone(), &self.preview) - .map(|_| ()) - .map_err(|_| RunnerCapabilityError::Request)?; - if self.request.operation_kind().is_cutover() { - let Some(consent) = self.consent.as_ref() else { - return Err(RunnerCapabilityError::Consent); - }; - let Some(recovery) = self.recovery.as_ref() else { - return Err(RunnerCapabilityError::Recovery); - }; - let Some(host_digest) = self.host_digest.as_ref() else { - return Err(RunnerCapabilityError::Recovery); - }; - if consent.binding() != &self.request.consent_binding() - || recovery - .digest() - .map_err(|_| RunnerCapabilityError::Recovery)? - != *self - .request - .recovery_digest() - .ok_or(RunnerCapabilityError::Recovery)? - || recovery.candidate_id() != self.request.candidate_id() - || recovery.preview_digest() != self.request.preview_digest() - || recovery.operator_id() != self.request.operator_id() - { - return Err(RunnerCapabilityError::Recovery); - } - let _ = host_digest; - } - if matches!( - self.request.inventory(), - crate::OperationInventory::Reset(inventory) - if inventory.allows_destroy_durable_volumes() - ) { - let Some(consent) = self.destructive_consent.as_ref() else { - return Err(RunnerCapabilityError::Consent); - }; - if consent.binding() != &self.request.consent_binding() { - return Err(RunnerCapabilityError::Consent); - } - } - Ok(()) - } - - /// Render the exact canonical bootstrap bytes. - pub fn canonical_bytes(&self) -> Result, RunnerCapabilityError> { - self.validate()?; - canonical_json_bytes(self).map_err(|_| RunnerCapabilityError::CanonicalJson) - } - - /// Decode and consume a bootstrap from a trusted fd payload. - pub fn decode_and_consume( - bytes: &[u8], - now_ms: u64, - ledger: &mut CapabilityLedger, - ) -> Result<(Self, ConsumedCapability), RunnerCapabilityError> { - CanonicalJsonValue::parse(bytes).map_err(|_| RunnerCapabilityError::CanonicalJson)?; - let bootstrap: Self = - serde_json::from_slice(bytes).map_err(|_| RunnerCapabilityError::Malformed)?; - let capability_bytes = bootstrap - .capability - .canonical_bytes() - .map_err(|_| RunnerCapabilityError::CanonicalJson)?; - let consumed = BootstrapCapability::decode_and_consume(&capability_bytes, now_ms, ledger)?; - bootstrap.validate()?; - Ok((bootstrap, consumed)) - } -} - -/// Durable runner paths derived from one opaque operation id. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RunnerPaths { - root: PathBuf, - operation_dir: PathBuf, - socket_dir: PathBuf, - /// Root-owned journal path. - pub journal: PathBuf, - /// Host-wide OFD lock path. - pub lock: PathBuf, - /// Owner-authenticated status socket. - pub socket: PathBuf, -} - -impl RunnerPaths { - /// Derive anchored paths without touching the filesystem. - pub fn new(root: impl Into, operation_id: &OperationId) -> Self { - let root = root.into(); - Self::new_with_socket_root(root.clone(), root, operation_id) - } - - /// Derive state paths and a separate runtime socket path. - pub fn new_with_socket_root( - root: impl Into, - socket_root: impl Into, - operation_id: &OperationId, - ) -> Self { - let root = root.into(); - let operation_dir = root.join("cutover").join(operation_id.as_str()); - let socket_dir = socket_root - .into() - .join("cutover") - .join(operation_id.as_str()); - Self { - journal: operation_dir.join("journal.json"), - lock: operation_dir.join("operation.lock"), - socket: socket_dir.join("runner.sock"), - root, - operation_dir, - socket_dir, - } - } - - /// Create the private operation directory with a bounded mode. - pub fn ensure_directory(&self) -> io::Result<()> { - ensure_owned_directory(&self.operation_dir, 0o700) - } - - /// Borrow the operation directory. - pub fn operation_dir(&self) -> &Path { - &self.operation_dir - } - - /// Borrow the runtime socket directory. - pub fn socket_dir(&self) -> &Path { - &self.socket_dir - } - - /// Borrow the state root. - pub fn root(&self) -> &Path { - &self.root - } -} - -/// Acquire the operation's host-wide OFD lock. -pub fn acquire_operation_lock(paths: &RunnerPaths) -> Result { - paths.ensure_directory().map_err(|_| RunnerLockError::Io)?; - if let Ok(metadata) = fs::symlink_metadata(&paths.lock) - && (!metadata.is_file() - || metadata.uid() != geteuid().as_raw() - || metadata.permissions().mode() & 0o777 != 0o600) - { - return Err(RunnerLockError::Io); - } - let file = OpenOptions::new() - .create(true) - .read(true) - .write(true) - .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) - .mode(0o600) - .open(&paths.lock) - .map_err(|_| RunnerLockError::Io)?; - let lock = libc::flock { - l_type: libc::F_WRLCK as i16, - l_whence: libc::SEEK_SET as i16, - l_start: 0, - l_len: 0, - l_pid: 0, - }; - fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&lock)) - .map_err(|_| RunnerLockError::Contended)?; - Ok(file) -} - -/// Persist a root-only runner journal envelope atomically. -pub fn persist_journal(path: &Path, bootstrap: &RunnerBootstrap, records: &[u8]) -> io::Result<()> { - let parent = path.parent().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "runner journal has no parent") - })?; - if let Ok(metadata) = fs::symlink_metadata(path) - && (!metadata.is_file() - || metadata.uid() != geteuid().as_raw() - || metadata.permissions().mode() & 0o777 != 0o600) - { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "runner journal has foreign ownership", - )); - } - fs::create_dir_all(parent)?; - let record_values = canonical_record_values(records)?; - let payload = serde_json::json!({ - "bootstrap": bootstrap, - "records": record_values, - }); - let bytes = canonical_json_bytes(&payload).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "runner journal is not canonical", - ) - })?; - if bytes.len() > MAX_RUNNER_FRAME_BYTES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "runner journal exceeds size limit", - )); - } - let temp = path.with_extension("journal.tmp"); - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) - .mode(0o600) - .open(&temp)?; - file.write_all(&bytes)?; - file.sync_all()?; - drop(file); - fs::rename(&temp, path)?; - File::open(parent)?.sync_all()?; - fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; - Ok(()) -} - -/// Read and validate a runner journal envelope without exposing it to a CLI. -pub fn load_journal(path: &Path) -> io::Result<(RunnerBootstrap, Vec)> { - let file = OpenOptions::new() - .read(true) - .custom_flags(libc::O_NOFOLLOW) - .open(path)?; - let metadata = file.metadata()?; - if metadata.uid() != geteuid().as_raw() || metadata.permissions().mode() & 0o777 != 0o600 { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "runner journal mode is not 0600", - )); - } - let mut bytes = Vec::new(); - file.take((MAX_RUNNER_FRAME_BYTES + 1) as u64) - .read_to_end(&mut bytes)?; - if bytes.len() > MAX_RUNNER_FRAME_BYTES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "runner journal exceeds size limit", - )); - } - let value: serde_json::Value = serde_json::from_slice(&bytes) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner journal malformed"))?; - CanonicalJsonValue::parse(&bytes) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner journal not canonical"))?; - let bootstrap: RunnerBootstrap = - serde_json::from_value(value.get("bootstrap").cloned().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "runner bootstrap missing") - })?) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner bootstrap malformed"))?; - bootstrap - .validate() - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner bootstrap invalid"))?; - let record_values: Vec = value - .get("records") - .cloned() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "runner journal records missing")) - .and_then(|value| { - serde_json::from_value(value).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "runner journal records malformed", - ) - }) - })?; - let mut records = Vec::new(); - for value in record_values { - records.extend(canonical_json_bytes(&value).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "runner journal record not canonical", - ) - })?); - records.push(b'\n'); - } - Journal::from_bytes( - JournalBinding::new( - bootstrap.request.operation_id().clone(), - bootstrap.request.revision_plan_id().clone(), - bootstrap.request.request_digest().clone(), - ), - &records, - ) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner journal chain invalid"))?; - Ok((bootstrap, records)) -} - -fn canonical_record_values(records: &[u8]) -> io::Result> { - if records.is_empty() { - return Ok(Vec::new()); - } - if !records.ends_with(b"\n") { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "runner journal records are truncated", - )); - } - records - .split(|byte| *byte == b'\n') - .filter(|line| !line.is_empty()) - .map(|line| { - CanonicalJsonValue::parse(line).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "runner journal record not canonical", - ) - }) - }) - .collect() -} - -/// One owner-socket command. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", tag = "command", deny_unknown_fields)] -pub enum RunnerCommand { - /// Read redacted operation status. - Status, - /// Apply one typed closure activation after consent and drain admission. - Apply { - /// Existing typed host-generation handoff contract. - handoff: d2b_contracts_broker::host_generation::ApplyHostGenerationHandoff, - }, - /// Dispatch one closed U3 effect through the adapted broker peer. - Effect { - /// Stable effect identity. - effect_id: EffectId, - /// Stable step identity. - step_id: StepId, - /// Closed U3 effect kind. - kind: EffectKind, - /// Crash-replay class. - replay_class: ReplayClass, - /// Phase reached after durable completion. - advance_to: Option, - /// Existing identity for identity-bearing effects. - #[serde(default, skip_serializing_if = "Option::is_none")] - identity: Option, - /// Typed generation handoff for closure activation. - #[serde(default, skip_serializing_if = "Option::is_none")] - handoff: Option, - /// Existing typed broker operation payload. - #[serde(default, skip_serializing_if = "Option::is_none")] - payload: Option, - }, - /// Roll back while the native rollback boundary is still open. - Rollback { - /// Optional typed handoff that restores the pre-apply generation. - #[serde(default, skip_serializing_if = "Option::is_none")] - handoff: Option, - }, - /// Request phase-9 verification through the runner boundary. - Verify { - /// Authoritative post-activation observations. - observations: RunnerVerificationInput, - }, - /// Consume the separately issued phase-10 finalization consent. - Finalize { - /// Canonical finalization consent artifact. - consent: crate::FinalizationConsent, - /// Approved artifact disposition payload. - plan: FinalizationPlan, - }, - /// Request an operator hold. - Hold { - /// Bounded operator reason. - reason: String, - }, - /// Resume after an owner or fresh-consent check. - Resume { - /// Optional digest-bound fresh consent for a non-owner Admin. - #[serde(default)] - fresh_consent: Option, - }, -} - -/// Redaction-safe status returned over the owner socket. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RunnerStatus { - /// Opaque operation identity. - pub operation_id: OperationId, - /// Preview digest admitted for this operation. - pub preview_digest: Digest, - /// Current pure-engine state. - pub state: OperationState, - /// Current phase. - pub phase: CutoverPhase, - /// Whether a hold blocks the next effect. - pub hold_active: bool, - /// Whether the runner has reached a terminal outcome. - pub terminal: bool, -} - -/// Response envelope for owner-socket commands. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RunnerResponse { - /// Whether the command was accepted. - pub accepted: bool, - /// Redacted status snapshot. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, - /// Stable failure code, never a raw error string. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -/// Stable owner-socket refusal classes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum RunnerSocketError { - /// The peer is not an admitted lifecycle identity. - Unauthorized, - /// The peer is not the bound operator or did not supply fresh consent. - OperatorMismatch, - /// The handoff artifact does not match the admitted cutover binding. - ArtifactBindingMismatch, - /// The command is not valid for the current operation state. - InvalidTransition, - /// The command frame was malformed or oversized. - Malformed, - /// The privileged audit sink did not return durable evidence. - AuditUnavailable, - /// The journal could not be durably advanced. - JournalUnavailable, -} - -/// Owner-authenticated runner socket. -pub struct RunnerSocket { - listener: UnixListener, - capability: ConsumedCapability, -} - -fn ensure_owned_directory(path: &Path, mode: u32) -> io::Result<()> { - let owner = geteuid().as_raw(); - let mut missing = Vec::new(); - let mut current = path; - loop { - match fs::symlink_metadata(current) { - Ok(metadata) => { - if !metadata.is_dir() || metadata.uid() != owner { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "runner directory has foreign ownership", - )); - } - if current == path && metadata.permissions().mode() & 0o777 != mode { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "runner directory has an unexpected mode", - )); - } - break; - } - Err(error) if error.kind() == io::ErrorKind::NotFound => { - missing.push(current.to_path_buf()); - current = current.parent().ok_or_else(|| { - io::Error::new( - io::ErrorKind::PermissionDenied, - "runner directory has no owned parent", - ) - })?; - } - Err(error) => return Err(error), - } - } - for directory in missing.into_iter().rev() { - fs::create_dir(&directory)?; - fs::set_permissions(&directory, fs::Permissions::from_mode(mode))?; - } - Ok(()) -} - -fn validate_runtime_anchor(path: &Path) -> io::Result<()> { - let metadata = fs::symlink_metadata(path)?; - if !metadata.is_dir() || metadata.uid() != 0 { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "runtime socket anchor has foreign ownership", - )); - } - Ok(()) -} - -fn validate_runtime_directory(path: &Path, gid: u32) -> io::Result<()> { - let metadata = fs::symlink_metadata(path)?; - if !metadata.is_dir() - || metadata.uid() != 0 - || metadata.gid() != gid - || metadata.permissions().mode() & 0o777 != 0o710 - { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "runtime socket directory has foreign ownership or mode", - )); - } - Ok(()) -} - -fn ensure_runtime_directory(path: &Path, gid: u32) -> io::Result<()> { - match fs::symlink_metadata(path) { - Ok(_) => validate_runtime_directory(path, gid), - Err(error) if error.kind() == io::ErrorKind::NotFound => { - let parent = path.parent().ok_or_else(|| { - io::Error::new( - io::ErrorKind::PermissionDenied, - "runtime socket directory has no parent", - ) - })?; - validate_runtime_anchor(parent)?; - fs::create_dir(path)?; - fs::set_permissions(path, fs::Permissions::from_mode(0o710))?; - chown(path, Some(Uid::from_raw(0)), Some(Gid::from_raw(gid))) - .map_err(|error| io::Error::from_raw_os_error(error as i32))?; - validate_runtime_directory(path, gid) - } - Err(error) => Err(error), - } -} - -fn ensure_runtime_socket_directories(paths: &RunnerPaths, gid: u32) -> io::Result<()> { - let shared = paths.socket_dir().parent().ok_or_else(|| { - io::Error::new( - io::ErrorKind::PermissionDenied, - "runtime socket directory has no shared parent", - ) - })?; - ensure_runtime_directory(shared, gid)?; - ensure_runtime_directory(paths.socket_dir(), gid) -} - -fn validate_socket_path(path: &Path, lifecycle_gid: Option) -> io::Result<()> { - let metadata = fs::symlink_metadata(path)?; - let expected_uid = if lifecycle_gid.is_some() { - 0 - } else { - geteuid().as_raw() - }; - let expected_mode = if lifecycle_gid.is_some() { - 0o660 - } else { - 0o600 - }; - if !metadata.file_type().is_socket() - || metadata.uid() != expected_uid - || lifecycle_gid.is_some_and(|gid| metadata.gid() != gid) - || metadata.permissions().mode() & 0o777 != expected_mode - { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "runner socket has foreign ownership or mode", - )); - } - Ok(()) -} - -impl RunnerSocket { - /// Bind one owner-authenticated socket. Existing foreign paths fail closed. - pub fn bind(paths: &RunnerPaths, capability: ConsumedCapability) -> io::Result { - paths.ensure_directory()?; - let lifecycle_gid = capability.lifecycle_gid(); - if let Some(gid) = lifecycle_gid { - ensure_runtime_socket_directories(paths, gid)?; - } else { - ensure_owned_directory(paths.socket_dir(), 0o700)?; - } - - match fs::symlink_metadata(&paths.socket) { - Ok(_) => { - validate_socket_path(&paths.socket, lifecycle_gid)?; - fs::remove_file(&paths.socket)?; - } - Err(error) if error.kind() == io::ErrorKind::NotFound => {} - Err(error) => return Err(error), - } - let listener = UnixListener::bind(&paths.socket)?; - let socket_mode = if lifecycle_gid.is_some() { - 0o660 - } else { - 0o600 - }; - fs::set_permissions(&paths.socket, fs::Permissions::from_mode(socket_mode))?; - if let Some(gid) = lifecycle_gid { - chown( - &paths.socket, - Some(Uid::from_raw(0)), - Some(Gid::from_raw(gid)), - ) - .map_err(|error| io::Error::from_raw_os_error(error as i32))?; - } - validate_socket_path(&paths.socket, lifecycle_gid)?; - Ok(Self { - listener, - capability, - }) - } - - /// Return the bound socket path. - pub fn path(&self) -> io::Result { - self.listener.local_addr().and_then(|addr| { - addr.as_pathname() - .map(PathBuf::from) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "unnamed runner socket")) - }) - } - - /// Accept one authenticated request and return its typed command. - pub fn accept_command(&self) -> io::Result<(UnixStream, RunnerCommand, RunnerPeer)> { - let (mut stream, _) = self.listener.accept()?; - let credentials = getsockopt(&stream, PeerCredentials) - .map_err(|error| io::Error::from_raw_os_error(error as i32))?; - authorize_peer(credentials.uid(), &self.capability, &RunnerCommand::Status).map_err( - |error| io::Error::new(io::ErrorKind::PermissionDenied, format!("{error:?}")), - )?; - let command = read_json_frame(&mut stream)?; - let peer = - authorize_peer(credentials.uid(), &self.capability, &command).map_err(|error| { - io::Error::new(io::ErrorKind::PermissionDenied, format!("{error:?}")) - })?; - Ok((stream, command, peer)) - } - - /// Authenticate a peer for one command without accepting a socket. - pub fn authorize( - &self, - peer_uid: u32, - command: &RunnerCommand, - ) -> Result { - authorize_peer(peer_uid, &self.capability, command) - } -} - -/// The peer class proven by lifecycle `SO_PEERCRED`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RunnerPeer { - /// The bound operator. - Owner, - /// Another configured Admin. - Admin, -} - -fn authorize_peer( - peer_uid: u32, - capability: &ConsumedCapability, - command: &RunnerCommand, -) -> Result { - let peer = if peer_uid == capability.operator_uid() { - RunnerPeer::Owner - } else if capability.is_admin_uid(peer_uid) { - RunnerPeer::Admin - } else { - return Err(RunnerSocketError::Unauthorized); - }; - match command { - RunnerCommand::Status => Ok(peer), - RunnerCommand::Apply { .. } => { - if matches!(peer, RunnerPeer::Owner) { - Ok(peer) - } else { - Err(RunnerSocketError::OperatorMismatch) - } - } - RunnerCommand::Effect { .. } - | RunnerCommand::Rollback { .. } - | RunnerCommand::Verify { .. } - | RunnerCommand::Finalize { .. } => { - if matches!(peer, RunnerPeer::Owner) { - Ok(peer) - } else { - Err(RunnerSocketError::OperatorMismatch) - } - } - RunnerCommand::Hold { .. } => Ok(peer), - RunnerCommand::Resume { fresh_consent } => { - if matches!(peer, RunnerPeer::Owner) || fresh_consent.is_some() { - Ok(peer) - } else { - Err(RunnerSocketError::OperatorMismatch) - } - } - } -} - -fn read_json_frame(stream: &mut UnixStream) -> io::Result { - let mut bytes = Vec::new(); - stream - .take((MAX_RUNNER_FRAME_BYTES + 1) as u64) - .read_to_end(&mut bytes)?; - if bytes.len() > MAX_RUNNER_FRAME_BYTES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "runner command exceeds size limit", - )); - } - serde_json::from_slice(&bytes) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner command malformed")) -} - -/// Send one owner-socket command and decode its response. -pub fn send_command(path: &Path, command: &RunnerCommand) -> io::Result { - let mut stream = UnixStream::connect(path)?; - stream.set_write_timeout(Some(RUNNER_SOCKET_TIMEOUT))?; - stream.set_read_timeout(Some(RUNNER_SOCKET_TIMEOUT))?; - let bytes = serde_json::to_vec(command) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner command encode failed"))?; - stream.write_all(&bytes)?; - stream.shutdown(std::net::Shutdown::Write)?; - let mut response = Vec::new(); - stream - .take((MAX_RUNNER_FRAME_BYTES + 1) as u64) - .read_to_end(&mut response)?; - if response.len() > MAX_RUNNER_FRAME_BYTES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "runner response exceeds size limit", - )); - } - serde_json::from_slice(&response) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner response malformed")) -} - -/// Write one owner-socket response. -pub fn write_response(stream: &mut UnixStream, response: &RunnerResponse) -> io::Result<()> { - let bytes = serde_json::to_vec(response) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner response encode failed"))?; - stream.write_all(&bytes) -} - -/// Runner capability validation failures. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RunnerCapabilityError { - /// Canonical JSON was rejected. - CanonicalJson, - /// The JSON shape was not the closed contract. - Malformed, - /// The protocol version is unsupported. - Version, - /// The capability lifetime is invalid. - InvalidLifetime, - /// The capability is expired at consumption time. - Expired, - /// The capability nonce was already consumed. - AlreadyConsumed, - /// The effect allowlist did not match the operation kind. - EffectAllowlistMismatch, - /// The bootstrap identities did not match. - IdentityMismatch, - /// The bootstrap preview did not match the operation request. - Preview, - /// The operation request was not accepted by the U3 engine. - Request, - /// Exact apply consent was not present or bound. - Consent, - /// Qualified recovery evidence was not present or bound. - Recovery, -} - -impl std::fmt::Display for RunnerCapabilityError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(match self { - Self::CanonicalJson => "cutover bootstrap capability is not canonical JSON", - Self::Malformed => "cutover bootstrap capability is malformed", - Self::Version => "cutover bootstrap capability version is unsupported", - Self::InvalidLifetime => "cutover bootstrap capability lifetime is invalid", - Self::Expired => "cutover bootstrap capability expired", - Self::AlreadyConsumed => "cutover bootstrap capability already consumed", - Self::EffectAllowlistMismatch => { - "cutover bootstrap capability effect allowlist mismatch" - } - Self::IdentityMismatch => "cutover bootstrap capability identity mismatch", - Self::Preview => "cutover bootstrap capability preview mismatch", - Self::Request => "cutover bootstrap capability request rejected", - Self::Consent => "cutover bootstrap apply consent rejected", - Self::Recovery => "cutover bootstrap recovery evidence rejected", - }) - } -} - -impl std::error::Error for RunnerCapabilityError {} - -/// Operation lock failures. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RunnerLockError { - /// Filesystem I/O failed. - Io, - /// Another operation owns the OFD lock. - Contended, -} - -impl std::fmt::Display for RunnerLockError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(match self { - Self::Io => "cutover operation lock I/O failed", - Self::Contended => "cutover operation lock is already held", - }) - } -} - -impl std::error::Error for RunnerLockError {} - -fn validate_time_window( - issued_at_ms: u64, - expires_at_ms: u64, -) -> Result<(), RunnerCapabilityError> { - if expires_at_ms <= issued_at_ms - || expires_at_ms.saturating_sub(issued_at_ms) > MAX_BOOTSTRAP_LIFETIME_MS - { - return Err(RunnerCapabilityError::InvalidLifetime); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn operation_paths_keep_opaque_ids_anchored() { - let id = OperationId::new("op-path-test").expect("id"); - let paths = RunnerPaths::new("/var/lib/d2b", &id); - assert_eq!( - paths.socket, - PathBuf::from("/var/lib/d2b/cutover/op-path-test/runner.sock") - ); - assert!(!paths.socket.to_string_lossy().contains("..")); - } - - #[test] - fn runtime_socket_directory_contract_is_fail_closed_and_traversal_only() { - let source = include_str!("runner.rs"); - assert!(source.contains("ensure_runtime_socket_directories(paths, gid)")); - assert!(source.contains("metadata.gid() != gid")); - assert!(source.contains("metadata.permissions().mode() & 0o777 != 0o710")); - assert!(source.contains("metadata.permissions().mode() & 0o777 != expected_mode")); - assert!(!source.contains("chown(\n paths.socket_dir()")); - } -} diff --git a/packages/d2b-cutover/src/state_machine.rs b/packages/d2b-cutover/src/state_machine.rs deleted file mode 100644 index c8c45eded3..0000000000 --- a/packages/d2b-cutover/src/state_machine.rs +++ /dev/null @@ -1,1456 +0,0 @@ -//! Pure resumable operation state machine. - -use std::collections::BTreeSet; -use std::fmt; - -use d2b_contracts_resource::v3::{CanonicalJsonError, canonical_digest, canonical_json_bytes}; -use serde::Serialize; - -use crate::{ - consent::{ - Consent, ConsentBinding, ConsentError, FinalizationBinding, FinalizationConsent, - RecoveryAttestation, - }, - finalize::{FinalizationError, require_cutover_finalization}, - hold::{HoldError, HoldState}, - inventory::{HostInventory, InventoryError}, - journal::{Journal, JournalBinding, JournalError}, - model::{ - ArtifactId, AuditRecordId, CandidateId, CompletionEvidence, CutoverPhase, Digest, EffectId, - EffectKind, EffectOutcome, FailureCode, HoldReason, HostLockContract, LockAcquire, - OperationId, OperationKind, OperationState, OperatorId, ReplayClass, ReplayDecision, - ReplayObservation, RevisionPlanId, StepId, TerminalOutcomeKind, - }, - preview::{CutoverPreview, PreviewError, PreviewInventory}, - reset::{ResetError, ResetInventory}, - rollback::{RollbackError, RollbackResult, plan_native_rollback}, - verify::{VerificationError, VerificationInput, VerificationReport, verify_cutover}, -}; - -/// Domain separator for operation request digests. -pub const REQUEST_DOMAIN: &str = "d2b:cutover:request:v1"; - -/// Inventory carried by one operation request. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase", tag = "kind", content = "value")] -pub enum OperationInventory { - /// Host-wide all-Zone cutover inventory. - Host(HostInventory), - /// Scoped reset inventory. - Reset(ResetInventory), -} - -/// Immutable operation request binding. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct OperationRequest { - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - operator_id: OperatorId, - #[serde(default, skip_serializing_if = "Option::is_none")] - system_artifact_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_system_artifact_id: Option, - preview_digest: Digest, - recovery_digest: Option, - inventory: OperationInventory, - inventory_digest: Digest, - request_digest: Digest, -} - -impl OperationRequest { - /// Construct a request and derive its digest from the exact inventory. - #[allow(clippy::too_many_arguments)] - pub fn new( - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - operator_id: OperatorId, - preview_digest: Digest, - recovery_digest: Option, - inventory: OperationInventory, - ) -> Result { - match (operation_kind, &inventory) { - (OperationKind::Cutover, OperationInventory::Host(_)) - | (OperationKind::ScopedReset(_), OperationInventory::Reset(_)) => {} - _ => return Err(OperationError::InventoryKindMismatch), - } - let inventory_digest = match &inventory { - OperationInventory::Host(inventory) => inventory.digest()?, - OperationInventory::Reset(inventory) => inventory.digest()?, - }; - let payload = OperationRequestPayload { - operation_id: operation_id.clone(), - operation_kind, - candidate_id: candidate_id.clone(), - revision_plan_id: revision_plan_id.clone(), - operator_id: operator_id.clone(), - system_artifact_id: None, - source_system_artifact_id: None, - preview_digest: preview_digest.clone(), - recovery_digest: recovery_digest.clone(), - inventory_digest: inventory_digest.clone(), - }; - let bytes = canonical_json_bytes(&payload).map_err(OperationError::CanonicalJson)?; - let request_digest = Digest::parse(canonical_digest(REQUEST_DOMAIN, &bytes)) - .map_err(|_| OperationError::Digest)?; - Ok(Self { - operation_id, - operation_kind, - candidate_id, - revision_plan_id, - operator_id, - system_artifact_id: None, - source_system_artifact_id: None, - preview_digest, - recovery_digest, - inventory, - inventory_digest, - request_digest, - }) - } - - /// Construct a host-wide cutover request. - #[allow(clippy::too_many_arguments)] - pub fn new_cutover( - operation_id: OperationId, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - operator_id: OperatorId, - preview_digest: Digest, - recovery_digest: Digest, - inventory: HostInventory, - ) -> Result { - Self::new( - operation_id, - OperationKind::Cutover, - candidate_id, - revision_plan_id, - operator_id, - preview_digest, - Some(recovery_digest), - OperationInventory::Host(inventory), - ) - } - - /// Construct a scoped reset request. - #[allow(clippy::too_many_arguments)] - pub fn new_reset( - operation_id: OperationId, - scope: crate::model::ResetScope, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - operator_id: OperatorId, - preview_digest: Digest, - inventory: ResetInventory, - ) -> Result { - if inventory.scope() != scope { - return Err(OperationError::Reset(ResetError::ScopeMismatch)); - } - Self::new( - operation_id, - OperationKind::ScopedReset(scope), - candidate_id, - revision_plan_id, - operator_id, - preview_digest, - None, - OperationInventory::Reset(inventory), - ) - } - - /// Borrow the operation identity. - pub fn operation_id(&self) -> &OperationId { - &self.operation_id - } - - /// Return the operation kind. - pub const fn operation_kind(&self) -> OperationKind { - self.operation_kind - } - - /// Borrow the candidate identity. - pub fn candidate_id(&self) -> &CandidateId { - &self.candidate_id - } - - /// Borrow the revision-plan identity. - pub fn revision_plan_id(&self) -> &RevisionPlanId { - &self.revision_plan_id - } - - /// Borrow the bound operator. - pub fn operator_id(&self) -> &OperatorId { - &self.operator_id - } - - /// Borrow the optional system artifact identity bound to a handoff. - pub fn system_artifact_id(&self) -> Option<&ArtifactId> { - self.system_artifact_id.as_ref() - } - - /// Bind a typed system artifact identity to this frozen request contract. - pub fn with_system_artifact_id( - mut self, - system_artifact_id: ArtifactId, - ) -> Result { - self.system_artifact_id = Some(system_artifact_id); - self.request_digest = self.derive_request_digest()?; - Ok(self) - } - - /// Borrow the preserved source artifact identity bound to native rollback. - pub fn source_system_artifact_id(&self) -> Option<&ArtifactId> { - self.source_system_artifact_id.as_ref() - } - - /// Bind the preserved source artifact identity to this frozen request. - pub fn with_source_system_artifact_id( - mut self, - source_system_artifact_id: ArtifactId, - ) -> Result { - self.source_system_artifact_id = Some(source_system_artifact_id); - self.request_digest = self.derive_request_digest()?; - Ok(self) - } - - /// Borrow the preview digest. - pub fn preview_digest(&self) -> &Digest { - &self.preview_digest - } - - /// Borrow the optional recovery digest. - pub fn recovery_digest(&self) -> Option<&Digest> { - self.recovery_digest.as_ref() - } - - /// Borrow the operation inventory. - pub fn inventory(&self) -> &OperationInventory { - &self.inventory - } - - /// Borrow the normalized inventory digest. - pub fn inventory_digest(&self) -> &Digest { - &self.inventory_digest - } - - /// Borrow the request digest. - pub fn request_digest(&self) -> &Digest { - &self.request_digest - } - - /// Verify that the request digest covers every immutable request field. - pub fn request_digest_matches(&self) -> Result { - Ok(self.derive_request_digest()? == self.request_digest) - } - - /// Build the exact apply-consent binding. - pub fn consent_binding(&self) -> ConsentBinding { - ConsentBinding::new( - self.operation_id.clone(), - self.operation_kind, - self.candidate_id.clone(), - self.preview_digest.clone(), - self.recovery_digest.clone(), - self.operator_id.clone(), - ) - .with_system_artifact_id(self.system_artifact_id.clone()) - .with_source_system_artifact_id(self.source_system_artifact_id.clone()) - } - - /// Build the distinct phase-10 binding. - pub fn finalization_binding(&self) -> FinalizationBinding { - FinalizationBinding::new( - self.operation_id.clone(), - self.operation_kind, - self.candidate_id.clone(), - self.preview_digest.clone(), - self.operator_id.clone(), - ) - } - - fn derive_request_digest(&self) -> Result { - let payload = OperationRequestPayload { - operation_id: self.operation_id.clone(), - operation_kind: self.operation_kind, - candidate_id: self.candidate_id.clone(), - revision_plan_id: self.revision_plan_id.clone(), - operator_id: self.operator_id.clone(), - system_artifact_id: self.system_artifact_id.clone(), - source_system_artifact_id: self.source_system_artifact_id.clone(), - preview_digest: self.preview_digest.clone(), - recovery_digest: self.recovery_digest.clone(), - inventory_digest: self.inventory_digest.clone(), - }; - let bytes = canonical_json_bytes(&payload).map_err(OperationError::CanonicalJson)?; - Digest::parse(canonical_digest(REQUEST_DOMAIN, &bytes)).map_err(|_| OperationError::Digest) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(rename_all = "camelCase")] -struct OperationRequestPayload { - operation_id: OperationId, - operation_kind: OperationKind, - candidate_id: CandidateId, - revision_plan_id: RevisionPlanId, - operator_id: OperatorId, - #[serde(default, skip_serializing_if = "Option::is_none")] - system_artifact_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_system_artifact_id: Option, - preview_digest: Digest, - recovery_digest: Option, - inventory_digest: Digest, -} - -/// Validation context sampled at apply or resume. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApplyContext { - now_ms: u64, - inventory_digest: Digest, - candidate_current: bool, - markers_valid: bool, - ownership_valid: bool, - recovery: Option, - host_digest: Option, -} - -impl ApplyContext { - /// Construct a cutover validation context. - pub fn cutover( - now_ms: u64, - inventory_digest: Digest, - candidate_current: bool, - markers_valid: bool, - ownership_valid: bool, - recovery: RecoveryAttestation, - host_digest: Digest, - ) -> Self { - Self { - now_ms, - inventory_digest, - candidate_current, - markers_valid, - ownership_valid, - recovery: Some(recovery), - host_digest: Some(host_digest), - } - } - - /// Construct a reset validation context. - pub fn reset( - now_ms: u64, - inventory_digest: Digest, - candidate_current: bool, - markers_valid: bool, - ownership_valid: bool, - ) -> Self { - Self { - now_ms, - inventory_digest, - candidate_current, - markers_valid, - ownership_valid, - recovery: None, - host_digest: None, - } - } -} - -/// Evidence required to complete a read-only phase. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ReadOnlyEvidence { - /// Whether the phase predicates held at the sampled boundary. - pub predicates_hold: bool, - /// Durable audit publication for the phase. - pub audit: crate::model::AuditEvidence, -} - -/// One effect request handed to a typed external adapter. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EffectRequest { - effect_id: EffectId, - step_id: StepId, - kind: EffectKind, - replay_class: ReplayClass, - identity_bearing: bool, - journaled_identity: Option, - destination: Option, - advance_to: Option, -} - -impl EffectRequest { - /// Construct an effect request with a closed replay class. - pub fn new( - effect_id: EffectId, - step_id: StepId, - kind: EffectKind, - replay_class: ReplayClass, - advance_to: Option, - ) -> Self { - Self { - effect_id, - step_id, - kind, - replay_class, - identity_bearing: false, - journaled_identity: None, - destination: None, - advance_to, - } - } - - /// Mark the effect as identity-bearing and bind an optional known identity. - pub fn with_identity( - mut self, - journaled_identity: Option, - destination: Option, - ) -> Self { - self.identity_bearing = true; - self.journaled_identity = journaled_identity; - self.destination = destination; - self - } - - /// Borrow the effect identity. - pub fn effect_id(&self) -> &EffectId { - &self.effect_id - } - - /// Borrow the step identity. - pub fn step_id(&self) -> &StepId { - &self.step_id - } - - /// Return the effect kind. - pub const fn kind(&self) -> EffectKind { - self.kind - } - - /// Return the replay class. - pub const fn replay_class(&self) -> ReplayClass { - self.replay_class - } - - /// Return whether the effect creates or adopts an identity-bearing object. - pub const fn identity_bearing(&self) -> bool { - self.identity_bearing - } - - /// Borrow the journaled identity, if already known. - pub fn journaled_identity(&self) -> Option<&ArtifactId> { - self.journaled_identity.as_ref() - } - - /// Borrow the staged destination identity, if any. - pub fn destination(&self) -> Option<&ArtifactId> { - self.destination.as_ref() - } - - /// Return the next phase after durable completion. - pub const fn advance_to(&self) -> Option { - self.advance_to - } - - /// Return whether another request is the same durable replay attempt. - pub fn matches_replay(&self, other: &Self) -> bool { - self.effect_id == other.effect_id - && self.step_id == other.step_id - && self.kind == other.kind - && self.replay_class == other.replay_class - && self.identity_bearing == other.identity_bearing - && self.journaled_identity == other.journaled_identity - && self.advance_to == other.advance_to - && (other.destination.is_none() || self.destination == other.destination) - } -} - -/// The resumable pure engine. -#[derive(Debug, Clone)] -pub struct Operation { - request: OperationRequest, - phase: CutoverPhase, - state: OperationState, - journal: Journal, - hold: HoldState, - current_effect: Option, - staged_destinations: BTreeSet, - terminal: Option, - rollback: Option, - lock_held: bool, -} - -/// Alias for the host-wide cutover engine. -pub type CutoverEngine = Operation; -/// Alias for the scoped reset engine. -pub type ResetEngine = Operation; - -impl Operation { - /// Create a planned operation after preview/request identity checks. - pub fn new( - request: OperationRequest, - preview: &CutoverPreview, - ) -> Result { - let preview_digest = preview.digest()?; - if preview.operation_id() != request.operation_id() - || preview.operation_kind() != request.operation_kind() - || preview.candidate_id() != request.candidate_id() - || preview.revision_plan_id() != request.revision_plan_id() - || &preview_digest != request.preview_digest() - { - return Err(OperationError::PreviewMismatch); - } - match (&request.inventory, preview.inventory()) { - ( - OperationInventory::Host(request_inventory), - PreviewInventory::Host(preview_inventory), - ) if request_inventory != preview_inventory => { - return Err(OperationError::PreviewMismatch); - } - ( - OperationInventory::Reset(request_inventory), - PreviewInventory::Reset(preview_inventory), - ) if request_inventory != preview_inventory => { - return Err(OperationError::PreviewMismatch); - } - (OperationInventory::Host(_), PreviewInventory::Reset(_)) - | (OperationInventory::Reset(_), PreviewInventory::Host(_)) => { - return Err(OperationError::PreviewMismatch); - } - _ => {} - } - let binding = JournalBinding::new( - request.operation_id.clone(), - request.revision_plan_id.clone(), - request.request_digest.clone(), - ); - Ok(Self { - request, - phase: CutoverPhase::Preflight, - state: OperationState::Planned, - journal: Journal::new(binding), - hold: HoldState::Clear, - current_effect: None, - staged_destinations: BTreeSet::new(), - terminal: None, - rollback: None, - lock_held: false, - }) - } - - /// Reopen an operation from a verified journal without replaying mutation. - pub fn reopen( - request: OperationRequest, - preview: &CutoverPreview, - journal: Journal, - ) -> Result { - let mut operation = Self::new(request, preview)?; - if journal.binding() != operation.journal.binding() { - return Err(OperationError::Journal(JournalError::RequestMismatch)); - } - for record in journal.records() { - match record.kind() { - crate::journal::JournalRecordKind::ConsentConsumed => { - operation.phase = record.phase(); - operation.state = if record.phase() == CutoverPhase::Finalization { - OperationState::Finalizing - } else { - OperationState::Applying(operation.phase) - }; - operation.lock_held = true; - } - crate::journal::JournalRecordKind::PhaseCompleted => { - if record.phase() == CutoverPhase::Verification { - operation.phase = CutoverPhase::Verification; - operation.state = OperationState::CutoverSucceeded; - } else { - operation.phase = record - .phase() - .next() - .ok_or(OperationError::InvalidTransition)?; - operation.state = OperationState::Applying(operation.phase); - } - operation.lock_held = true; - } - crate::journal::JournalRecordKind::Started => { - let (Some(effect_id), Some(step_id), Some(effect_kind), Some(replay_class)) = ( - record.effect_id(), - record.step_id(), - record.effect_kind(), - record.replay_class(), - ) else { - return Err(OperationError::Journal(JournalError::Malformed)); - }; - operation.phase = record.phase(); - let mut effect = EffectRequest::new( - effect_id.clone(), - step_id.clone(), - effect_kind, - replay_class, - record.next_phase(), - ); - if let Some(identity) = record.identity() { - effect = effect - .with_identity(Some(identity.clone()), record.destination().cloned()); - } - if let Some(destination) = record.destination() { - operation.staged_destinations.insert(destination.clone()); - } - operation.current_effect = Some(effect); - operation.state = if operation.phase == CutoverPhase::Finalization { - OperationState::Finalizing - } else { - OperationState::Applying(operation.phase) - }; - operation.lock_held = true; - } - crate::journal::JournalRecordKind::Completed => { - let Some(record_effect_id) = record.effect_id() else { - return Err(OperationError::Journal(JournalError::Malformed)); - }; - if operation - .current_effect - .as_ref() - .is_some_and(|effect| effect.effect_id() == record_effect_id) - { - operation.current_effect = None; - } - if let Some(destination) = record.destination() { - operation.staged_destinations.insert(destination.clone()); - } - operation.phase = record.next_phase().unwrap_or(record.phase()); - if let HoldState::Pending { - requested_by, - reason, - } = std::mem::replace(&mut operation.hold, HoldState::Clear) - { - operation.hold = HoldState::Active { - requested_by, - reason, - }; - operation.state = OperationState::Held; - } else { - operation.state = OperationState::Applying(operation.phase); - } - operation.lock_held = true; - } - crate::journal::JournalRecordKind::RetryableFailure => { - let Some(record_effect_id) = record.effect_id() else { - return Err(OperationError::Journal(JournalError::Malformed)); - }; - if operation - .current_effect - .as_ref() - .is_some_and(|effect| effect.effect_id() == record_effect_id) - { - operation.current_effect = None; - } - if let Some(destination) = record.destination() { - operation.staged_destinations.insert(destination.clone()); - } - operation.phase = record.phase(); - operation.state = if let HoldState::Pending { - requested_by, - reason, - } = - std::mem::replace(&mut operation.hold, HoldState::Clear) - { - operation.hold = HoldState::Active { - requested_by, - reason, - }; - OperationState::Held - } else { - OperationState::Applying(operation.phase) - }; - operation.lock_held = true; - } - crate::journal::JournalRecordKind::HoldRequested => { - let reason = - HoldReason::new("journaled-hold").map_err(|_| OperationError::Digest)?; - let operator = operation.request.operator_id().clone(); - if operation.current_effect.is_some() { - operation.hold = HoldState::Pending { - requested_by: operator, - reason, - }; - operation.state = OperationState::Applying(operation.phase); - } else { - operation.hold = HoldState::Active { - requested_by: operator, - reason, - }; - operation.state = OperationState::Held; - } - operation.lock_held = true; - } - crate::journal::JournalRecordKind::HoldCleared => { - operation.hold = HoldState::Clear; - operation.phase = record.phase(); - operation.state = OperationState::Applying(operation.phase); - operation.lock_held = true; - } - crate::journal::JournalRecordKind::Terminal => { - let Some(outcome) = record.terminal_outcome() else { - return Err(OperationError::Journal(JournalError::Malformed)); - }; - operation.current_effect = None; - operation.terminal = Some(outcome); - operation.state = match outcome { - TerminalOutcomeKind::RolledBack => OperationState::RolledBack, - TerminalOutcomeKind::RestoreRequired => OperationState::RestoreRequired, - TerminalOutcomeKind::Closed => OperationState::Closed, - TerminalOutcomeKind::Failed => OperationState::Failed, - }; - operation.lock_held = true; - } - } - } - operation.journal = journal; - Ok(operation) - } - - /// Borrow the immutable request. - pub fn request(&self) -> &OperationRequest { - &self.request - } - - /// Borrow the preview digest admitted for this operation. - pub fn preview_digest(&self) -> &Digest { - self.request.preview_digest() - } - - /// Return the current phase. - pub const fn phase(&self) -> CutoverPhase { - self.phase - } - - /// Return the current state. - pub const fn state(&self) -> OperationState { - self.state - } - - /// Borrow the journal. - pub fn journal(&self) -> &Journal { - &self.journal - } - - /// Render the current journal bytes. - pub fn journal_bytes(&self) -> Result, OperationError> { - Ok(self.journal.to_bytes()?) - } - - /// Borrow the current hold state. - pub fn hold(&self) -> &HoldState { - &self.hold - } - - /// Borrow the in-flight effect. - pub fn current_effect(&self) -> Option<&EffectRequest> { - self.current_effect.as_ref() - } - - /// Borrow staged destinations that are retained only as non-authoritative data. - pub fn staged_destinations(&self) -> impl Iterator { - self.staged_destinations.iter() - } - - /// Return the write-once terminal outcome, if present. - pub const fn terminal_outcome(&self) -> Option { - self.terminal - } - - /// Borrow the native rollback result, if rollback completed. - pub fn rollback_result(&self) -> Option<&RollbackResult> { - self.rollback.as_ref() - } - - /// Attempt to acquire the one host-wide linearization lock. - pub fn acquire_host_lock(&mut self, lock: &mut HostLockContract) -> Result<(), OperationError> { - match lock.acquire(self.request.operation_id()) { - LockAcquire::Acquired => { - self.lock_held = true; - Ok(()) - } - LockAcquire::AlreadyOwned => Ok(()), - LockAcquire::Contended(owner) => Err(OperationError::LockContended(owner)), - } - } - - /// Release the model lock after the operation is terminal. - pub fn release_host_lock(&mut self, lock: &mut HostLockContract) -> Result<(), OperationError> { - if !self.state.is_terminal() && self.state != OperationState::CutoverSucceeded { - return Err(OperationError::InvalidTransition); - } - self.lock_held = false; - if lock.release(self.request.operation_id()) { - Ok(()) - } else { - Err(OperationError::LockNotHeld) - } - } - - /// Consume exact apply consent after revalidating all boundary evidence. - pub fn begin_apply( - &mut self, - consent: &mut Consent, - context: &ApplyContext, - ) -> Result<(), OperationError> { - if self.state != OperationState::Planned || !self.lock_held { - return Err(OperationError::InvalidTransition); - } - self.validate_context(context)?; - consent - .consume(&self.request.consent_binding(), context.now_ms) - .map_err(OperationError::Consent)?; - self.journal.append_consent(self.phase)?; - self.state = OperationState::Applying(self.phase); - Ok(()) - } - - /// Complete a read-only phase only after its audit record is durable. - pub fn complete_read_only_phase( - &mut self, - phase: CutoverPhase, - evidence: ReadOnlyEvidence, - ) -> Result<(), OperationError> { - if self.state != OperationState::Applying(phase) - || phase.number() > CutoverPhase::Inventory.number() - { - return Err(OperationError::InvalidTransition); - } - if !evidence.predicates_hold { - return Err(OperationError::PhaseEvidence); - } - let audit_id = durable_audit_id(&evidence.audit)?; - self.journal.append_phase_completed(phase, audit_id)?; - let next = phase.next().ok_or(OperationError::InvalidTransition)?; - self.phase = next; - self.state = OperationState::Applying(next); - Ok(()) - } - - /// Start one effect, writing `started` before the adapter may mutate. - pub fn start_effect(&mut self, effect: EffectRequest) -> Result<(), OperationError> { - if !matches!( - self.state, - OperationState::Applying(_) | OperationState::Finalizing - ) || self.current_effect.is_some() - || self.hold.blocks_new_effects() - { - return Err(OperationError::InvalidTransition); - } - if !self - .request - .operation_kind() - .allowlist() - .permits(effect.kind()) - { - return Err(OperationError::EffectNotAllowed(effect.kind())); - } - if effect.kind() == EffectKind::CutoverFinalization - && self.state != OperationState::Finalizing - { - return Err(OperationError::FinalizationConsentRequired); - } - if self.state == OperationState::Finalizing - && effect.kind() != EffectKind::CutoverFinalization - { - return Err(OperationError::EffectNotAllowed(effect.kind())); - } - if effect.kind() == EffectKind::DestroyDurableVolume - && matches!( - &self.request.inventory, - OperationInventory::Reset(inventory) - if !inventory.allows_destroy_durable_volumes() - ) - { - return Err(OperationError::EffectNotAllowed(effect.kind())); - } - if effect.identity_bearing() - && effect.replay_class() != ReplayClass::ReopenByJournaledIdentity - { - return Err(OperationError::IdentityReplayClassRequired); - } - if effect.identity_bearing() && effect.journaled_identity().is_none() { - return Err(OperationError::IdentityMismatch); - } - self.journal.append_started_with_destination( - self.phase, - effect.step_id.clone(), - effect.effect_id.clone(), - effect.kind, - effect.advance_to, - effect.replay_class, - effect.journaled_identity.clone(), - effect.destination.clone(), - )?; - self.current_effect = Some(effect); - Ok(()) - } - - /// Mark the current effect as retryable without retracting its started evidence. - pub fn retry_effect(&mut self, failure_code: FailureCode) -> Result<(), OperationError> { - let Some(effect) = self.current_effect.clone() else { - return Err(OperationError::NoCurrentEffect); - }; - self.journal.append_retryable_failure( - self.phase, - effect.step_id.clone(), - effect.effect_id.clone(), - effect.kind, - effect.advance_to, - effect.replay_class, - effect.journaled_identity.clone(), - effect.destination.clone(), - failure_code, - )?; - self.current_effect = None; - self.state = if let HoldState::Pending { - requested_by, - reason, - } = std::mem::replace(&mut self.hold, HoldState::Clear) - { - self.hold = HoldState::Active { - requested_by, - reason, - }; - OperationState::Held - } else { - OperationState::Applying(self.phase) - }; - Ok(()) - } - - /// Complete an effect only when both effect and audit evidence are durable. - pub fn complete_effect( - &mut self, - effect_id: &EffectId, - evidence: CompletionEvidence, - ) -> Result<(), OperationError> { - let Some(effect) = self.current_effect.clone() else { - return Err(OperationError::NoCurrentEffect); - }; - if effect.effect_id() != effect_id { - return Err(OperationError::EffectMismatch); - } - match evidence.effect.outcome() { - EffectOutcome::Succeeded => {} - EffectOutcome::Failed => return Err(OperationError::EffectFailed), - EffectOutcome::Ambiguous => return Err(OperationError::DestinationAmbiguous), - } - let audit_id = durable_audit_id(&evidence.audit)?; - if effect.identity_bearing() { - let Some(observed) = evidence.effect.identity() else { - return Err(OperationError::IdentityMismatch); - }; - if effect - .journaled_identity() - .is_some_and(|expected| expected != observed) - { - return Err(OperationError::IdentityMismatch); - } - } - self.journal.append_completed_with_destination( - self.phase, - effect.step_id.clone(), - effect.effect_id.clone(), - effect.kind, - effect.advance_to, - effect.replay_class, - evidence.effect.identity().cloned(), - effect.destination.clone(), - audit_id, - )?; - if let Some(destination) = effect.destination() { - self.staged_destinations.insert(destination.clone()); - } - self.current_effect = None; - if self.state == OperationState::Finalizing { - self.journal.append_terminal( - self.phase, - TerminalOutcomeKind::Closed, - AuditRecordId::new("phase-10-terminal").map_err(|_| OperationError::Digest)?, - )?; - self.terminal = Some(TerminalOutcomeKind::Closed); - self.state = OperationState::Closed; - return Ok(()); - } - if let Some(next) = effect.advance_to() { - self.phase = next; - } - if let HoldState::Pending { - requested_by, - reason, - } = std::mem::replace(&mut self.hold, HoldState::Clear) - { - self.hold = HoldState::Active { - requested_by, - reason, - }; - self.state = OperationState::Held; - } else { - self.state = OperationState::Applying(self.phase); - } - Ok(()) - } - - /// Request a hold; an in-flight atomic effect is allowed to finish first. - pub fn request_hold( - &mut self, - requested_by: OperatorId, - reason: crate::model::HoldReason, - audit: crate::model::AuditEvidence, - ) -> Result<(), OperationError> { - if self.state.is_terminal() { - return Err(OperationError::Hold(HoldError::Terminal)); - } - let audit_id = durable_audit_id(&audit)?; - self.journal.append_hold_requested(self.phase, audit_id)?; - if self.current_effect.is_some() { - self.hold = HoldState::Pending { - requested_by, - reason, - }; - } else { - self.hold = HoldState::Active { - requested_by, - reason, - }; - self.state = OperationState::Held; - } - Ok(()) - } - - /// Resume after a hold, revalidating the same candidate and inventory. - pub fn resume( - &mut self, - operator: &OperatorId, - context: &ApplyContext, - audit: crate::model::AuditEvidence, - ) -> Result<(), OperationError> { - let Some(requested_by) = self.hold.requested_by() else { - return Err(OperationError::Hold(HoldError::NotActive)); - }; - if requested_by != operator && self.request.operator_id() != operator { - return Err(OperationError::Hold(HoldError::OperatorMismatch)); - } - if self.state != OperationState::Held { - return Err(OperationError::InvalidTransition); - } - self.validate_context(context)?; - let audit_id = durable_audit_id(&audit)?; - self.journal.append_hold_cleared(self.phase, audit_id)?; - self.hold = HoldState::Clear; - self.state = OperationState::Applying(self.phase); - Ok(()) - } - - /// Classify replay of the current started effect without mutating a host. - pub fn replay_decision( - &self, - observation: ReplayObservation, - ) -> Result { - let Some(effect) = self.current_effect.as_ref() else { - let Some(record) = self.journal.incomplete_effect() else { - return Err(OperationError::NoCurrentEffect); - }; - return replay_for_record(record.replay_class(), record.identity(), observation); - }; - replay_for_record( - Some(effect.replay_class()), - effect.journaled_identity(), - observation, - ) - } - - /// Verify all Zones and move to the separate finalization boundary. - pub fn verify( - &mut self, - input: &VerificationInput, - ) -> Result { - if self.request.operation_kind() != OperationKind::Cutover - || self.state != OperationState::Applying(CutoverPhase::Verification) - { - return Err(OperationError::InvalidTransition); - } - let inventory = match self.request.inventory() { - OperationInventory::Host(inventory) => inventory, - OperationInventory::Reset(_) => return Err(OperationError::InventoryKindMismatch), - }; - let report = verify_cutover(inventory, input)?; - self.journal.append_phase_completed( - CutoverPhase::Verification, - AuditRecordId::new("phase-9-verification").map_err(|_| OperationError::Digest)?, - )?; - self.state = OperationState::CutoverSucceeded; - Ok(report) - } - - /// Consume the separate phase-10 consent. - pub fn begin_finalization( - &mut self, - consent: &mut FinalizationConsent, - now_ms: u64, - ) -> Result<(), OperationError> { - require_cutover_finalization(self.request.operation_kind(), CutoverPhase::Finalization) - .map_err(OperationError::Finalization)?; - if self.state != OperationState::CutoverSucceeded - && self.state != OperationState::Finalizing - { - return Err(OperationError::FinalizationConsentRequired); - } - if self.state == OperationState::Finalizing { - consent - .consume(&self.request.finalization_binding(), now_ms) - .map_err(OperationError::Consent)?; - return Ok(()); - } - consent - .consume(&self.request.finalization_binding(), now_ms) - .map_err(OperationError::Consent)?; - self.phase = CutoverPhase::Finalization; - self.journal.append_consent(self.phase)?; - self.state = OperationState::Finalizing; - Ok(()) - } - - /// Perform native rollback through phase 4 only. - pub fn rollback( - &mut self, - audit: crate::model::AuditEvidence, - ) -> Result { - if !matches!( - self.state, - OperationState::Applying(_) | OperationState::Held - ) { - return Err(OperationError::InvalidTransition); - } - if self.current_effect.is_some() { - return Err(OperationError::InvalidTransition); - } - let inventory = match self.request.inventory() { - OperationInventory::Host(inventory) => inventory, - OperationInventory::Reset(_) => return Err(OperationError::InventoryKindMismatch), - }; - let result = plan_native_rollback( - self.phase, - inventory, - self.staged_destinations.iter().cloned(), - )?; - let audit_id = durable_audit_id(&audit)?; - self.journal - .append_terminal(self.phase, TerminalOutcomeKind::RolledBack, audit_id)?; - self.terminal = Some(TerminalOutcomeKind::RolledBack); - self.state = OperationState::RolledBack; - self.rollback = Some(result.clone()); - Ok(result) - } - - /// Publish a terminal external-restore outcome after phase 5 failure. - pub fn require_external_restore( - &mut self, - audit: crate::model::AuditEvidence, - ) -> Result<(), OperationError> { - if self.phase.is_before_or_at_native_rollback_boundary() { - return Err(OperationError::Rollback(RollbackError::BoundaryClosed( - self.phase, - ))); - } - self.current_effect = None; - self.write_terminal(TerminalOutcomeKind::RestoreRequired, audit) - } - - /// Publish a write-once terminal failure. - pub fn fail_terminal( - &mut self, - _reason: FailureCode, - audit: crate::model::AuditEvidence, - ) -> Result<(), OperationError> { - self.write_terminal(TerminalOutcomeKind::Failed, audit) - } - - fn write_terminal( - &mut self, - outcome: TerminalOutcomeKind, - audit: crate::model::AuditEvidence, - ) -> Result<(), OperationError> { - if self.terminal.is_some() { - return Err(OperationError::TerminalAlreadyWritten); - } - let audit_id = durable_audit_id(&audit)?; - self.journal - .append_terminal(self.phase, outcome, audit_id)?; - self.terminal = Some(outcome); - self.state = match outcome { - TerminalOutcomeKind::RolledBack => OperationState::RolledBack, - TerminalOutcomeKind::RestoreRequired => OperationState::RestoreRequired, - TerminalOutcomeKind::Closed => OperationState::Closed, - TerminalOutcomeKind::Failed => OperationState::Failed, - }; - Ok(()) - } - - fn validate_context(&self, context: &ApplyContext) -> Result<(), OperationError> { - if &context.inventory_digest != self.request.inventory_digest() - || !context.candidate_current - || !context.markers_valid - || !context.ownership_valid - { - return Err(OperationError::ContextMismatch); - } - if self.request.operation_kind().is_cutover() { - let Some(recovery) = context.recovery.as_ref() else { - return Err(OperationError::Recovery(ConsentError::RecoveryMismatch)); - }; - let Some(host_digest) = context.host_digest.as_ref() else { - return Err(OperationError::Recovery(ConsentError::RecoveryMismatch)); - }; - recovery - .validate_for( - self.request.candidate_id(), - self.request.preview_digest(), - self.request.operator_id(), - host_digest, - context.now_ms, - ) - .map_err(OperationError::Recovery)?; - if Some(recovery.digest().map_err(OperationError::Consent)?) - != self.request.recovery_digest - { - return Err(OperationError::Recovery(ConsentError::RecoveryMismatch)); - } - } - Ok(()) - } -} - -fn durable_audit_id(audit: &crate::model::AuditEvidence) -> Result { - if !audit.is_durable() { - return Err(OperationError::AuditNotDurable); - } - audit - .record_id() - .cloned() - .ok_or(OperationError::AuditNotDurable) -} - -fn replay_for_record( - replay_class: Option, - journaled_identity: Option<&ArtifactId>, - observation: ReplayObservation, -) -> Result { - let Some(replay_class) = replay_class else { - return Err(OperationError::NoCurrentEffect); - }; - Ok(match replay_class { - ReplayClass::Repeatable => match observation { - ReplayObservation::Absent => ReplayDecision::Repeat, - _ => ReplayDecision::Quarantine(FailureCode::DestinationAmbiguous), - }, - ReplayClass::ReopenByJournaledIdentity => { - let Some(expected) = journaled_identity else { - return Ok(ReplayDecision::Quarantine(FailureCode::IdentityMismatch)); - }; - match observation { - ReplayObservation::JournaledIdentity(observed) if &observed == expected => { - ReplayDecision::Reopen(observed) - } - ReplayObservation::WrongIdentity - | ReplayObservation::DuplicateIdentity - | ReplayObservation::InvalidMarker - | ReplayObservation::PartialDestination - | ReplayObservation::ReplacedDestination - | ReplayObservation::ForeignOwner - | ReplayObservation::Ambiguous - | ReplayObservation::JournaledIdentity(_) => { - ReplayDecision::Quarantine(FailureCode::IdentityMismatch) - } - ReplayObservation::Absent => { - ReplayDecision::Quarantine(FailureCode::IdentityMismatch) - } - } - } - ReplayClass::QuarantineOnly => { - ReplayDecision::Quarantine(FailureCode::DestinationAmbiguous) - } - }) -} - -/// Operation state-machine failure. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum OperationError { - /// A preview, request, or inventory did not match. - PreviewMismatch, - /// The operation kind and inventory kind were different. - InventoryKindMismatch, - /// Inventory construction failed. - Inventory(InventoryError), - /// Preview canonicalization failed. - Preview(PreviewError), - /// Consent validation failed. - Consent(ConsentError), - /// Recovery evidence failed. - Recovery(ConsentError), - /// Journal validation or append failed. - Journal(JournalError), - /// Canonical request encoding failed. - CanonicalJson(CanonicalJsonError), - /// A digest could not be represented. - Digest, - /// A state transition was not valid. - InvalidTransition, - /// The host-wide lock is owned by another operation. - LockContended(OperationId), - /// This operation does not hold the host lock. - LockNotHeld, - /// One effect was not authorized by the operation kind. - EffectNotAllowed(EffectKind), - /// Identity-bearing effects must use reopen replay semantics. - IdentityReplayClassRequired, - /// The requested effect is not the in-flight effect. - EffectMismatch, - /// No effect is currently in flight. - NoCurrentEffect, - /// The effect result was a failure. - EffectFailed, - /// The effect result was ambiguous. - DestinationAmbiguous, - /// The effect or audit evidence was incomplete. - AuditNotDurable, - /// An identity-bearing result was absent or mismatched. - IdentityMismatch, - /// Read-only phase predicates were not proven. - PhaseEvidence, - /// Apply or resume boundary evidence drifted. - ContextMismatch, - /// Hold transition failed. - Hold(HoldError), - /// Rollback boundary failed. - Rollback(RollbackError), - /// Reset authority rejected the request. - Reset(ResetError), - /// Verification failed. - Verification(VerificationError), - /// Finalization authority rejected the request. - Finalization(FinalizationError), - /// A terminal outcome was already written. - TerminalAlreadyWritten, - /// A reset or pre-finalization path attempted finalization. - FinalizationConsentRequired, -} - -impl OperationError { - /// Return the stable failure class. - pub const fn code(&self) -> FailureCode { - match self { - Self::PreviewMismatch | Self::InventoryKindMismatch | Self::ContextMismatch => { - FailureCode::CandidateDrift - } - Self::Inventory(error) => error.code(), - Self::Preview(_) | Self::CanonicalJson(_) | Self::Digest => { - FailureCode::RequestMismatch - } - Self::Consent(_) | Self::Recovery(_) => FailureCode::ConsentInvalid, - Self::Journal(error) => error.code(), - Self::InvalidTransition => FailureCode::InvalidTransition, - Self::LockContended(_) => FailureCode::LockContended, - Self::LockNotHeld => FailureCode::LockContended, - Self::EffectNotAllowed(_) | Self::Reset(_) | Self::Finalization(_) => { - FailureCode::EffectNotAllowed - } - Self::IdentityReplayClassRequired | Self::IdentityMismatch => { - FailureCode::IdentityMismatch - } - Self::EffectMismatch - | Self::NoCurrentEffect - | Self::EffectFailed - | Self::DestinationAmbiguous => FailureCode::EffectFailed, - Self::AuditNotDurable => FailureCode::AuditNotDurable, - Self::PhaseEvidence => FailureCode::InvalidTransition, - Self::Hold(_) => FailureCode::HoldActive, - Self::Rollback(error) => error.code(), - Self::Verification(error) => error.code(), - Self::TerminalAlreadyWritten => FailureCode::TerminalAlreadyWritten, - Self::FinalizationConsentRequired => FailureCode::FinalizationConsentRequired, - } - } -} - -impl fmt::Display for OperationError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::PreviewMismatch => "operation preview mismatch", - Self::InventoryKindMismatch => "operation inventory kind mismatch", - Self::Inventory(_) => "operation inventory rejected", - Self::Preview(_) => "operation preview rejected", - Self::Consent(_) => "operation consent rejected", - Self::Recovery(_) => "operation recovery evidence rejected", - Self::Journal(_) => "operation journal rejected", - Self::CanonicalJson(_) => "operation request canonicalization failed", - Self::Digest => "operation digest failed", - Self::InvalidTransition => "operation transition invalid", - Self::LockContended(_) => "host operation lock contended", - Self::LockNotHeld => "host operation lock not held", - Self::EffectNotAllowed(_) => "operation effect not allowed", - Self::IdentityReplayClassRequired => "identity effect replay class invalid", - Self::EffectMismatch => "operation effect mismatch", - Self::NoCurrentEffect => "operation has no current effect", - Self::EffectFailed => "operation effect failed", - Self::DestinationAmbiguous => "operation destination is ambiguous", - Self::AuditNotDurable => "operation audit is not durable", - Self::IdentityMismatch => "operation identity mismatch", - Self::PhaseEvidence => "operation phase evidence failed", - Self::ContextMismatch => "operation context drifted", - Self::Hold(_) => "operation hold transition failed", - Self::Rollback(_) => "operation rollback rejected", - Self::Reset(_) => "operation reset scope rejected", - Self::Verification(_) => "operation verification rejected", - Self::Finalization(_) => "operation finalization rejected", - Self::TerminalAlreadyWritten => "operation terminal outcome already written", - Self::FinalizationConsentRequired => "operation finalization consent required", - }) - } -} - -impl std::error::Error for OperationError {} - -impl From for OperationError { - fn from(error: InventoryError) -> Self { - Self::Inventory(error) - } -} - -impl From for OperationError { - fn from(error: PreviewError) -> Self { - Self::Preview(error) - } -} - -impl From for OperationError { - fn from(error: JournalError) -> Self { - Self::Journal(error) - } -} - -impl From for OperationError { - fn from(error: RollbackError) -> Self { - Self::Rollback(error) - } -} - -impl From for OperationError { - fn from(error: VerificationError) -> Self { - Self::Verification(error) - } -} - -impl From for OperationError { - fn from(error: ResetError) -> Self { - Self::Reset(error) - } -} diff --git a/packages/d2b-cutover/src/verify.rs b/packages/d2b-cutover/src/verify.rs deleted file mode 100644 index d10e08f71b..0000000000 --- a/packages/d2b-cutover/src/verify.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Pure verification and finalization eligibility contracts. - -use std::collections::BTreeSet; -use std::fmt; - -use crate::{ - inventory::HostInventory, - model::{FailureCode, ZoneId}, -}; - -/// Verification result for one Zone. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ZoneVerification { - zone_id: ZoneId, - healthy: bool, -} - -impl ZoneVerification { - /// Construct a Zone verification observation. - pub fn new(zone_id: ZoneId, healthy: bool) -> Self { - Self { zone_id, healthy } - } - - /// Borrow the Zone identity. - pub fn zone_id(&self) -> &ZoneId { - &self.zone_id - } - - /// Return whether this Zone passed. - pub const fn healthy(&self) -> bool { - self.healthy - } -} - -/// Pure inputs to the phase-9 verification contract. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VerificationInput { - zones: Vec, - sources_preserved: bool, - identity_digests_match: bool, - audit_durable: bool, - candidate_current: bool, -} - -impl VerificationInput { - /// Construct verification inputs. - pub fn new( - zones: impl IntoIterator, - sources_preserved: bool, - identity_digests_match: bool, - audit_durable: bool, - candidate_current: bool, - ) -> Self { - Self { - zones: zones.into_iter().collect(), - sources_preserved, - identity_digests_match, - audit_durable, - candidate_current, - } - } -} - -/// Successful verification proof. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VerificationReport { - zone_count: usize, - sources_preserved: bool, - identity_digests_match: bool, - audit_durable: bool, - candidate_current: bool, -} - -impl VerificationReport { - /// Return the number of verified Zones. - pub const fn zone_count(&self) -> usize { - self.zone_count - } - - /// Return whether source preservation passed. - pub const fn sources_preserved(&self) -> bool { - self.sources_preserved - } - - /// Return whether identity digests matched. - pub const fn identity_digests_match(&self) -> bool { - self.identity_digests_match - } - - /// Return whether the audit evidence was durable. - pub const fn audit_durable(&self) -> bool { - self.audit_durable - } - - /// Return whether the candidate remained current. - pub const fn candidate_current(&self) -> bool { - self.candidate_current - } -} - -/// Verify all configured Zones and identity-preservation invariants. -pub fn verify_cutover( - inventory: &HostInventory, - input: &VerificationInput, -) -> Result { - let expected = inventory.zone_ids().collect::>(); - let observed = input - .zones - .iter() - .map(|zone| zone.zone_id()) - .collect::>(); - if expected != observed { - return Err(VerificationError::ZoneSetMismatch); - } - if input.zones.iter().any(|zone| !zone.healthy()) { - return Err(VerificationError::ZoneUnhealthy); - } - if !input.sources_preserved { - return Err(VerificationError::SourcesNotPreserved); - } - if !input.identity_digests_match { - return Err(VerificationError::IdentityDigestMismatch); - } - if !input.audit_durable { - return Err(VerificationError::AuditNotDurable); - } - if !input.candidate_current { - return Err(VerificationError::CandidateDrift); - } - Ok(VerificationReport { - zone_count: expected.len(), - sources_preserved: input.sources_preserved, - identity_digests_match: input.identity_digests_match, - audit_durable: input.audit_durable, - candidate_current: input.candidate_current, - }) -} - -/// Verification failure. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VerificationError { - /// The observed Zone set did not exactly match configured Zones. - ZoneSetMismatch, - /// A Zone failed health verification. - ZoneUnhealthy, - /// A source artifact was not preserved. - SourcesNotPreserved, - /// An identity digest changed. - IdentityDigestMismatch, - /// The verification audit was not durable. - AuditNotDurable, - /// The candidate drifted before verification. - CandidateDrift, -} - -impl VerificationError { - /// Return the stable failure class. - pub const fn code(self) -> FailureCode { - match self { - Self::ZoneSetMismatch | Self::ZoneUnhealthy => FailureCode::VerificationIncomplete, - Self::SourcesNotPreserved => FailureCode::SourceNotPreserved, - Self::IdentityDigestMismatch => FailureCode::IdentityMismatch, - Self::AuditNotDurable => FailureCode::AuditNotDurable, - Self::CandidateDrift => FailureCode::CandidateDrift, - } - } -} - -impl fmt::Display for VerificationError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(match self { - Self::ZoneSetMismatch => "verification Zone set mismatch", - Self::ZoneUnhealthy => "verification Zone unhealthy", - Self::SourcesNotPreserved => "verification source was not preserved", - Self::IdentityDigestMismatch => "verification identity digest mismatch", - Self::AuditNotDurable => "verification audit is not durable", - Self::CandidateDrift => "verification candidate drift", - }) - } -} - -impl std::error::Error for VerificationError {} diff --git a/packages/d2b-cutover/tests/crash_recovery.rs b/packages/d2b-cutover/tests/crash_recovery.rs deleted file mode 100644 index 22c8898284..0000000000 --- a/packages/d2b-cutover/tests/crash_recovery.rs +++ /dev/null @@ -1,657 +0,0 @@ -use d2b_cutover::{ - ApplyContext, ArtifactId, AuditEvidence, CandidateId, CompletionEvidence, Consent, - CutoverEngine, CutoverPhase, CutoverPreview, Digest, EffectEvidence, EffectId, EffectKind, - EffectRequest, FailureCode, FinalizationConsent, HoldReason, HoldState, HostInventory, - HostLockContract, Journal, JournalBinding, JournalError, JournalRecordKind, OperationId, - OperationInventory, OperationKind, OperationRequest, OperationState, OperatorId, - RecoveryAttestation, RecoveryId, ReplayClass, ReplayDecision, ReplayObservation, - RevisionPlanId, StepId, VerificationInput, ZoneId, ZoneInventory, ZoneVerification, -}; - -fn digest(label: &str) -> Digest { - Digest::derive("d2b:test", label.as_bytes()) -} - -fn valid_evidence() -> Vec { - format!( - r#"{{"recoveryId":"recovery-1","candidateId":"candidate-1","hostDigest":"{}","previewDigest":"{}","operatorId":"operator-1","restoreInstructionsDigest":"{}","issuedAtMs":900,"expiresAtMs":1900,"qualified":true}}"#, - digest("host"), - digest("preview"), - digest("restore"), - ) - .into_bytes() -} - -fn engine() -> CutoverEngine { - let inventory = HostInventory::build( - [ZoneId::new("zone-a").unwrap()], - [ZoneInventory::empty("zone-a").unwrap()], - [], - ) - .unwrap(); - let operation_id = OperationId::new("operation-1").unwrap(); - let candidate_id = CandidateId::new("candidate-1").unwrap(); - let revision = RevisionPlanId::new("revision-1").unwrap(); - let operator = OperatorId::new("operator-1").unwrap(); - let preview = CutoverPreview::new( - operation_id.clone(), - OperationKind::Cutover, - candidate_id.clone(), - revision.clone(), - inventory.clone(), - None, - ) - .unwrap(); - let preview_digest = preview.digest().unwrap(); - let recovery = RecoveryAttestation::new( - RecoveryId::new("recovery-1").unwrap(), - candidate_id, - digest("host"), - preview_digest.clone(), - operator.clone(), - digest("restore"), - 900, - 1_900, - true, - ) - .unwrap(); - let request = OperationRequest::new_cutover( - operation_id, - CandidateId::new("candidate-1").unwrap(), - revision, - operator.clone(), - preview_digest, - recovery.digest().unwrap(), - inventory.clone(), - ) - .unwrap(); - let mut engine = CutoverEngine::new(request, &preview).unwrap(); - let mut lock = HostLockContract::new(); - engine.acquire_host_lock(&mut lock).unwrap(); - let context = ApplyContext::cutover( - 1_000, - inventory.digest().unwrap(), - true, - true, - true, - recovery, - digest("host"), - ); - let mut consent = Consent::issue(engine.request().consent_binding(), 900, 1_900).unwrap(); - engine.begin_apply(&mut consent, &context).unwrap(); - for phase in [ - CutoverPhase::Preflight, - CutoverPhase::Consent, - CutoverPhase::Inventory, - ] { - engine - .complete_read_only_phase( - phase, - d2b_cutover::ReadOnlyEvidence { - predicates_hold: true, - audit: AuditEvidence::durable(format!("audit-{phase:?}")).unwrap(), - }, - ) - .unwrap(); - } - engine -} - -fn journal_fixture() -> (JournalBinding, Journal) { - let binding = JournalBinding::new( - OperationId::new("operation-1").unwrap(), - RevisionPlanId::new("revision-1").unwrap(), - digest("request"), - ); - let mut journal = Journal::new(binding.clone()); - journal.append_consent(CutoverPhase::Preflight).unwrap(); - journal - .append_started( - CutoverPhase::Drain, - StepId::new("step-1").unwrap(), - EffectId::new("effect-1").unwrap(), - EffectKind::HostDrain, - Some(CutoverPhase::Disposition), - ReplayClass::Repeatable, - None, - ) - .unwrap(); - journal - .append_completed( - CutoverPhase::Drain, - StepId::new("step-1").unwrap(), - EffectId::new("effect-1").unwrap(), - EffectKind::HostDrain, - Some(CutoverPhase::Disposition), - ReplayClass::Repeatable, - None, - d2b_cutover::AuditRecordId::new("audit-1").unwrap(), - ) - .unwrap(); - (binding, journal) -} - -#[test] -fn strict_evidence_rejects_duplicate_unknown_trailing_fractional_negative_and_large_values() { - let valid = valid_evidence(); - assert!(RecoveryAttestation::decode_json(&valid).is_ok()); - let duplicate = valid - .strip_suffix(b"}") - .unwrap() - .iter() - .copied() - .chain(br#","qualified":true}"#.iter().copied()) - .collect::>(); - assert!(matches!( - RecoveryAttestation::decode_json(&duplicate), - Err(d2b_cutover::ConsentError::CanonicalJson(_)) - )); - - for malformed in [ - &br#"{"recoveryId":"recovery-1","candidateId":"candidate-1","hostDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","previewDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","operatorId":"operator-1","restoreInstructionsDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","issuedAtMs":900,"expiresAtMs":1900,"qualified":true,"unknown":1}"#[..], - &br#"{"recoveryId":"recovery-1","candidateId":"candidate-1","hostDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","previewDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","operatorId":"operator-1","restoreInstructionsDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","issuedAtMs":900.5,"expiresAtMs":1900,"qualified":true}"#[..], - &br#"{"recoveryId":"recovery-1","candidateId":"candidate-1","hostDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","previewDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","operatorId":"operator-1","restoreInstructionsDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","issuedAtMs":-1,"expiresAtMs":1900,"qualified":true}"#[..], - &br#"{"recoveryId":"recovery-1","candidateId":"candidate-1","hostDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","previewDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","operatorId":"operator-1","restoreInstructionsDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","issuedAtMs":900,"expiresAtMs":9223372036854775808,"qualified":true}"#[..], - &br#"{"recoveryId":"recovery-1","candidateId":"candidate-1","hostDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","previewDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","operatorId":"operator-1","restoreInstructionsDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","issuedAtMs":900,"expiresAtMs":1900,"qualified":true} trailing"#[..], - ] { - assert!(RecoveryAttestation::decode_json(malformed).is_err()); - } -} - -#[test] -fn journal_detects_bit_flip_truncation_reorder_and_request_mismatch() { - let (binding, journal) = journal_fixture(); - let bytes = journal.to_bytes().unwrap(); - assert_eq!( - Journal::from_bytes(binding.clone(), &bytes) - .unwrap() - .records() - .len(), - 3 - ); - - let mut bit_flip = bytes.clone(); - let offset = bit_flip - .windows(b"recordDigest\":\"sha256:".len()) - .position(|window| window == b"recordDigest\":\"sha256:") - .unwrap() - + b"recordDigest\":\"sha256:".len(); - bit_flip[offset] = if bit_flip[offset] == b'0' { b'1' } else { b'0' }; - assert_eq!( - Journal::from_bytes(binding.clone(), &bit_flip), - Err(JournalError::Tampered) - ); - assert_eq!( - Journal::from_bytes(binding.clone(), &bytes[..bytes.len() - 1]), - Err(JournalError::Truncated) - ); - - let mut lines = bytes.split(|byte| *byte == b'\n').collect::>(); - lines.pop(); - lines.reverse(); - let reordered = lines - .into_iter() - .flat_map(|line| line.iter().copied().chain(*b"\n")) - .collect::>(); - assert!(matches!( - Journal::from_bytes(binding.clone(), &reordered), - Err(JournalError::RequestMismatch | JournalError::Reordered | JournalError::Tampered) - )); - - let other_binding = JournalBinding::new( - OperationId::new("operation-other").unwrap(), - RevisionPlanId::new("revision-1").unwrap(), - digest("request"), - ); - assert_eq!( - Journal::from_bytes(other_binding, &bytes), - Err(JournalError::RequestMismatch) - ); -} - -#[test] -fn replay_classes_repeat_reopen_or_quarantine_without_creating_identity() { - let mut repeatable = engine(); - let repeatable_request = EffectRequest::new( - EffectId::new("effect-repeat").unwrap(), - StepId::new("step-repeat").unwrap(), - EffectKind::HostDrain, - ReplayClass::Repeatable, - None, - ); - repeatable.start_effect(repeatable_request).unwrap(); - assert_eq!( - repeatable - .replay_decision(ReplayObservation::Absent) - .unwrap(), - ReplayDecision::Repeat - ); - - let mut identity = engine(); - let identity_request = EffectRequest::new( - EffectId::new("effect-store").unwrap(), - StepId::new("step-store").unwrap(), - EffectKind::ResourceStoreCreate, - ReplayClass::ReopenByJournaledIdentity, - None, - ) - .with_identity( - Some(ArtifactId::new("store-uuid").unwrap()), - Some(ArtifactId::new("store-stage").unwrap()), - ); - identity.start_effect(identity_request).unwrap(); - assert_eq!( - identity - .replay_decision(ReplayObservation::JournaledIdentity( - ArtifactId::new("store-uuid").unwrap() - )) - .unwrap(), - ReplayDecision::Reopen(ArtifactId::new("store-uuid").unwrap()) - ); - for observation in [ - ReplayObservation::WrongIdentity, - ReplayObservation::DuplicateIdentity, - ReplayObservation::InvalidMarker, - ReplayObservation::PartialDestination, - ReplayObservation::ReplacedDestination, - ReplayObservation::ForeignOwner, - ReplayObservation::Ambiguous, - ReplayObservation::Absent, - ] { - assert_eq!( - identity.replay_decision(observation).unwrap(), - ReplayDecision::Quarantine(FailureCode::IdentityMismatch) - ); - } - - let mut quarantine = engine(); - quarantine - .start_effect(EffectRequest::new( - EffectId::new("effect-final").unwrap(), - StepId::new("step-final").unwrap(), - EffectKind::ClosureActivation, - ReplayClass::QuarantineOnly, - None, - )) - .unwrap(); - assert_eq!( - quarantine - .replay_decision(ReplayObservation::Absent) - .unwrap(), - ReplayDecision::Quarantine(FailureCode::DestinationAmbiguous) - ); -} - -#[test] -fn identity_completion_reopens_the_journaled_identity_and_never_creates_again() { - let mut engine = engine(); - let request = EffectRequest::new( - EffectId::new("effect-store").unwrap(), - StepId::new("step-store").unwrap(), - EffectKind::ResourceStoreCreate, - ReplayClass::ReopenByJournaledIdentity, - None, - ) - .with_identity( - Some(ArtifactId::new("store-uuid").unwrap()), - Some(ArtifactId::new("store-stage").unwrap()), - ); - engine.start_effect(request.clone()).unwrap(); - engine - .complete_effect( - request.effect_id(), - CompletionEvidence { - effect: EffectEvidence::succeeded_with_identity("store-uuid").unwrap(), - audit: AuditEvidence::durable("store-audit").unwrap(), - }, - ) - .unwrap(); - let started = engine - .journal() - .records() - .iter() - .find(|record| record.kind() == JournalRecordKind::Started) - .unwrap(); - let completed = engine - .journal() - .records() - .iter() - .find(|record| record.kind() == JournalRecordKind::Completed) - .unwrap(); - assert_eq!(started.effect_kind(), Some(EffectKind::ResourceStoreCreate)); - assert_eq!(completed.kind(), JournalRecordKind::Completed); - assert_eq!( - completed.identity(), - Some(&ArtifactId::new("store-uuid").unwrap()) - ); -} - -#[test] -fn restart_reopens_the_verified_journal_and_resumes_after_last_durable_success() { - let mut original = engine(); - let request = EffectRequest::new( - EffectId::new("effect-repeat").unwrap(), - StepId::new("step-repeat").unwrap(), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - original.start_effect(request).unwrap(); - let journal_bytes = original.journal_bytes().unwrap(); - let binding = original.journal().binding().clone(); - let journal = Journal::from_bytes(binding, &journal_bytes).unwrap(); - let request = original.request().clone(); - let inventory = match request.inventory() { - OperationInventory::Host(inventory) => inventory.clone(), - OperationInventory::Reset(_) => panic!("cutover fixture must use host inventory"), - }; - let preview = CutoverPreview::new( - request.operation_id().clone(), - OperationKind::Cutover, - request.candidate_id().clone(), - request.revision_plan_id().clone(), - inventory, - None, - ) - .unwrap(); - let mut reopened = CutoverEngine::reopen(request, &preview, journal).unwrap(); - assert!(reopened.current_effect().is_some()); - assert_eq!( - reopened.replay_decision(ReplayObservation::Absent).unwrap(), - ReplayDecision::Repeat - ); - let effect_id = reopened.current_effect().unwrap().effect_id().clone(); - reopened - .complete_effect( - &effect_id, - CompletionEvidence { - effect: EffectEvidence::succeeded(), - audit: AuditEvidence::durable("replay-audit").unwrap(), - }, - ) - .unwrap(); - assert_eq!(reopened.phase(), CutoverPhase::Disposition); -} - -#[test] -fn restart_after_verification_and_finalization_consent_preserves_finalizing_state() { - let mut original = engine(); - for (name, kind, replay_class, identity, advance_to) in [ - ( - "host-drain", - EffectKind::HostDrain, - ReplayClass::Repeatable, - None, - Some(CutoverPhase::Disposition), - ), - ( - "closure-activation", - EffectKind::ClosureActivation, - ReplayClass::ReopenByJournaledIdentity, - Some(ArtifactId::new("system-artifact").unwrap()), - Some(CutoverPhase::ResourceStore), - ), - ( - "resource-store", - EffectKind::ResourceStoreCreate, - ReplayClass::ReopenByJournaledIdentity, - Some(ArtifactId::new("store-identity").unwrap()), - Some(CutoverPhase::ProviderInstall), - ), - ( - "provider-install", - EffectKind::ProviderInstall, - ReplayClass::Repeatable, - None, - Some(CutoverPhase::ZoneCutover), - ), - ( - "zone-activation", - EffectKind::ZoneActivation, - ReplayClass::Repeatable, - None, - Some(CutoverPhase::Activation), - ), - ( - "guest-activation", - EffectKind::GuestActivation, - ReplayClass::Repeatable, - None, - Some(CutoverPhase::Verification), - ), - ] { - let mut effect = EffectRequest::new( - EffectId::new(format!("effect-{name}")).unwrap(), - StepId::new(format!("step-{name}")).unwrap(), - kind, - replay_class, - advance_to, - ); - if let Some(identity) = identity { - effect = effect.with_identity(Some(identity), None); - } - let effect_id = effect.effect_id().clone(); - let observed_identity = effect.journaled_identity().cloned(); - original.start_effect(effect).unwrap(); - original - .complete_effect( - &effect_id, - CompletionEvidence { - effect: match observed_identity { - Some(identity) => { - EffectEvidence::succeeded_with_identity(identity.as_str()).unwrap() - } - None => EffectEvidence::succeeded(), - }, - audit: AuditEvidence::durable(format!("audit-{name}")).unwrap(), - }, - ) - .unwrap(); - } - original - .verify(&VerificationInput::new( - [ZoneVerification::new(ZoneId::new("zone-a").unwrap(), true)], - true, - true, - true, - true, - )) - .unwrap(); - assert_eq!(original.state(), OperationState::CutoverSucceeded); - let mut consent = - FinalizationConsent::issue(original.request().finalization_binding(), 1_000, 1_900) - .unwrap(); - original.begin_finalization(&mut consent, 1_100).unwrap(); - assert_eq!(original.state(), OperationState::Finalizing); - - let finalization = EffectRequest::new( - EffectId::new("effect-finalization").unwrap(), - StepId::new("step-finalization").unwrap(), - EffectKind::CutoverFinalization, - ReplayClass::QuarantineOnly, - None, - ); - original.start_effect(finalization).unwrap(); - let journal = Journal::from_bytes( - original.journal().binding().clone(), - &original.journal_bytes().unwrap(), - ) - .unwrap(); - let request = original.request().clone(); - let inventory = match request.inventory() { - OperationInventory::Host(inventory) => inventory.clone(), - OperationInventory::Reset(_) => panic!("cutover fixture must use host inventory"), - }; - let preview = CutoverPreview::new( - request.operation_id().clone(), - OperationKind::Cutover, - request.candidate_id().clone(), - request.revision_plan_id().clone(), - inventory, - None, - ) - .unwrap(); - let mut reopened = CutoverEngine::reopen(request, &preview, journal).unwrap(); - assert_eq!(reopened.state(), OperationState::Finalizing); - let effect_id = reopened.current_effect().unwrap().effect_id().clone(); - reopened - .complete_effect( - &effect_id, - CompletionEvidence { - effect: EffectEvidence::succeeded(), - audit: AuditEvidence::durable("finalization-audit").unwrap(), - }, - ) - .unwrap(); - assert_eq!(reopened.state(), OperationState::Closed); -} - -#[test] -fn retryable_effect_failures_allow_multiple_attempts() { - let mut operation = engine(); - let effect = EffectRequest::new( - EffectId::new("effect-retry").unwrap(), - StepId::new("step-retry").unwrap(), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - operation.start_effect(effect.clone()).unwrap(); - operation.retry_effect(FailureCode::EffectFailed).unwrap(); - operation.start_effect(effect.clone()).unwrap(); - operation.retry_effect(FailureCode::EffectFailed).unwrap(); - assert!(operation.current_effect().is_none()); - - let journal = Journal::from_bytes( - operation.journal().binding().clone(), - &operation.journal_bytes().unwrap(), - ) - .unwrap(); - let request = operation.request().clone(); - let inventory = match request.inventory() { - OperationInventory::Host(inventory) => inventory.clone(), - OperationInventory::Reset(_) => panic!("cutover fixture must use host inventory"), - }; - let preview = CutoverPreview::new( - request.operation_id().clone(), - OperationKind::Cutover, - request.candidate_id().clone(), - request.revision_plan_id().clone(), - inventory, - None, - ) - .unwrap(); - let mut reopened = CutoverEngine::reopen(request, &preview, journal).unwrap(); - assert!(reopened.current_effect().is_none()); - reopened.start_effect(effect).unwrap(); -} - -#[test] -fn reopen_keeps_started_destination_for_native_rollback() { - let mut operation = engine(); - let drain = EffectRequest::new( - EffectId::new("effect-drain-destination").unwrap(), - StepId::new("step-drain-destination").unwrap(), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - operation.start_effect(drain.clone()).unwrap(); - operation - .complete_effect( - drain.effect_id(), - CompletionEvidence { - effect: EffectEvidence::succeeded(), - audit: AuditEvidence::durable("drain-audit").unwrap(), - }, - ) - .unwrap(); - let store = EffectRequest::new( - EffectId::new("effect-store-destination").unwrap(), - StepId::new("step-store-destination").unwrap(), - EffectKind::ResourceStoreCreate, - ReplayClass::ReopenByJournaledIdentity, - Some(CutoverPhase::ProviderInstall), - ) - .with_identity( - Some(ArtifactId::new("store-identity").unwrap()), - Some(ArtifactId::new("store-destination").unwrap()), - ); - operation.start_effect(store).unwrap(); - let journal = Journal::from_bytes( - operation.journal().binding().clone(), - &operation.journal_bytes().unwrap(), - ) - .unwrap(); - let request = operation.request().clone(); - let inventory = match request.inventory() { - OperationInventory::Host(inventory) => inventory.clone(), - OperationInventory::Reset(_) => panic!("cutover fixture must use host inventory"), - }; - let preview = CutoverPreview::new( - request.operation_id().clone(), - OperationKind::Cutover, - request.candidate_id().clone(), - request.revision_plan_id().clone(), - inventory, - None, - ) - .unwrap(); - let reopened = CutoverEngine::reopen(request, &preview, journal).unwrap(); - assert_eq!( - reopened - .current_effect() - .and_then(|effect| effect.destination()), - Some(&ArtifactId::new("store-destination").unwrap()) - ); - assert_eq!( - reopened.staged_destinations().collect::>(), - [&ArtifactId::new("store-destination").unwrap()] - ); -} - -#[test] -fn pending_hold_reopens_as_pending_while_effect_is_in_flight() { - let mut operation = engine(); - let effect = EffectRequest::new( - EffectId::new("effect-hold-reopen").unwrap(), - StepId::new("step-hold-reopen").unwrap(), - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - operation.start_effect(effect).unwrap(); - operation - .request_hold( - OperatorId::new("operator-1").unwrap(), - HoldReason::new("pause-before-restart").unwrap(), - AuditEvidence::durable("hold-audit").unwrap(), - ) - .unwrap(); - let journal = Journal::from_bytes( - operation.journal().binding().clone(), - &operation.journal_bytes().unwrap(), - ) - .unwrap(); - let request = operation.request().clone(); - let inventory = match request.inventory() { - OperationInventory::Host(inventory) => inventory.clone(), - OperationInventory::Reset(_) => panic!("cutover fixture must use host inventory"), - }; - let preview = CutoverPreview::new( - request.operation_id().clone(), - OperationKind::Cutover, - request.candidate_id().clone(), - request.revision_plan_id().clone(), - inventory, - None, - ) - .unwrap(); - let reopened = CutoverEngine::reopen(request, &preview, journal).unwrap(); - assert!(matches!(reopened.hold(), HoldState::Pending { .. })); - assert_eq!( - reopened.state(), - OperationState::Applying(CutoverPhase::Drain) - ); - assert!(reopened.current_effect().is_some()); -} diff --git a/packages/d2b-cutover/tests/reset_scope.rs b/packages/d2b-cutover/tests/reset_scope.rs deleted file mode 100644 index 2244421129..0000000000 --- a/packages/d2b-cutover/tests/reset_scope.rs +++ /dev/null @@ -1,308 +0,0 @@ -use d2b_cutover::{ - ApplyContext, ArtifactId, AuditEvidence, CandidateId, Consent, ConsentBinding, CutoverPhase, - CutoverPreview, Digest, Disposition, EffectAllowlist, EffectCapability, EffectId, EffectKind, - EffectRequest, HostInventory, HostLockContract, InventoryClass, InventoryError, - InventoryInputItem, InventoryItem, OperationInventory, OperationKind, OperationRequest, - OperationState, OperatorId, ResetEngine, ResetError, ResetInventory, ResetScope, - RevisionPlanId, StepId, ZoneId, ZoneInventory, -}; - -const NOW: u64 = 1_000; - -fn digest(label: &str) -> Digest { - Digest::derive("d2b:test", label.as_bytes()) -} - -fn inventory_in_order(reverse: bool) -> HostInventory { - let mut zones = vec![ - ZoneInventory::new( - "zone-a", - true, - [InventoryItem::classified( - "zone-a-volume", - InventoryClass::DurableVolume, - Disposition::Adopt, - ) - .unwrap() - .into()], - ) - .unwrap(), - ZoneInventory::new( - "zone-b", - true, - [InventoryItem::unclassified("unknown-b").unwrap().into()], - ) - .unwrap(), - ]; - if reverse { - zones.reverse(); - } - let mut configured = vec![ - ZoneId::new("zone-a").unwrap(), - ZoneId::new("zone-b").unwrap(), - ]; - if reverse { - configured.reverse(); - } - HostInventory::build( - configured, - zones, - [ - InventoryItem::classified("host-key", InventoryClass::SshKey, Disposition::Preserve) - .unwrap() - .into(), - ], - ) - .unwrap() -} - -#[test] -fn equivalent_all_zone_inventories_have_stable_preview_and_consent_digests() { - let first_inventory = inventory_in_order(false); - let second_inventory = inventory_in_order(true); - assert_eq!( - first_inventory.digest().unwrap(), - second_inventory.digest().unwrap() - ); - let first = CutoverPreview::new( - d2b_cutover::OperationId::new("operation-1").unwrap(), - OperationKind::Cutover, - CandidateId::new("candidate-1").unwrap(), - RevisionPlanId::new("revision-1").unwrap(), - first_inventory, - None, - ) - .unwrap(); - let second = CutoverPreview::new( - d2b_cutover::OperationId::new("operation-1").unwrap(), - OperationKind::Cutover, - CandidateId::new("candidate-1").unwrap(), - RevisionPlanId::new("revision-1").unwrap(), - second_inventory, - None, - ) - .unwrap(); - assert_eq!(first.digest().unwrap(), second.digest().unwrap()); - assert_eq!( - first.canonical_bytes().unwrap(), - second.canonical_bytes().unwrap() - ); - assert_eq!( - CutoverPreview::decode_json(&first.canonical_bytes().unwrap()) - .unwrap() - .digest() - .unwrap(), - first.digest().unwrap() - ); - let host = match first.inventory() { - d2b_cutover::PreviewInventory::Host(host) => host, - d2b_cutover::PreviewInventory::Reset(_) => panic!("cutover preview must be host-wide"), - }; - assert!(HostInventory::decode_json(&host.canonical_bytes().unwrap()).is_ok()); -} - -#[test] -fn partial_zone_inventory_and_gateway_custody_refuse_before_preview() { - let missing = HostInventory::build( - [ - ZoneId::new("zone-a").unwrap(), - ZoneId::new("zone-b").unwrap(), - ], - [ZoneInventory::empty("zone-a").unwrap()], - [], - ); - assert_eq!(missing, Err(InventoryError::PartialZoneInventory)); - - let gateway = ZoneInventory::new( - "zone-a", - true, - [InventoryInputItem::RealmGatewayCredentialAudit( - ArtifactId::new("gateway-private").unwrap(), - )], - ); - assert_eq!( - gateway, - Err(InventoryError::RealmGatewayCredentialAuditForbidden) - ); -} - -#[test] -fn reset_inventories_preserve_durable_volumes_and_have_distinct_effects() { - for scope in [ResetScope::Zone, ResetScope::Provider, ResetScope::Guest] { - let inventory = ResetInventory::new(scope, "target-1").unwrap(); - assert!(inventory.preserves_durable_volumes()); - assert!(!inventory.allows_destroy_durable_volumes()); - assert!( - ResetInventory::new(scope, "target-1") - .unwrap() - .with_preserve_durable_volumes(false) - .with_destroy_durable_consent(true) - .allows_destroy_durable_volumes() - ); - let kind = OperationKind::ScopedReset(scope); - let allowlist = EffectAllowlist::for_operation(kind); - assert!(!allowlist.permits(EffectKind::HostDrain)); - assert!(!allowlist.permits(EffectKind::ClosureActivation)); - assert!(!allowlist.permits(EffectKind::CutoverFinalization)); - assert!(!allowlist.permits(EffectKind::CutoverBroker)); - assert!(allowlist.permits(match scope { - ResetScope::Zone => EffectKind::ScopedZoneReset, - ResetScope::Provider => EffectKind::ScopedProviderReset, - ResetScope::Guest => EffectKind::ScopedGuestReset, - })); - } - - let capability = EffectCapability::new( - d2b_cutover::OperationId::new("reset-operation").unwrap(), - OperatorId::new("operator-1").unwrap(), - OperationKind::ScopedReset(ResetScope::Guest), - ); - assert_eq!( - capability.authorize(EffectKind::HostDrain), - Err(ResetError::EffectNotAllowed(EffectKind::HostDrain)) - ); -} - -#[test] -fn cutover_capability_does_not_authorize_scoped_reset_effects() { - let capability = EffectCapability::new( - d2b_cutover::OperationId::new("cutover-operation").unwrap(), - OperatorId::new("operator-1").unwrap(), - OperationKind::Cutover, - ); - assert_eq!( - capability.authorize(EffectKind::ScopedGuestReset), - Err(ResetError::EffectNotAllowed(EffectKind::ScopedGuestReset)) - ); - assert!(capability.authorize(EffectKind::HostDrain).is_ok()); -} - -#[test] -fn consent_is_single_use_and_cannot_transfer_to_a_new_operation() { - let binding = ConsentBinding::new( - d2b_cutover::OperationId::new("operation-1").unwrap(), - OperationKind::Cutover, - CandidateId::new("candidate-1").unwrap(), - digest("preview"), - Some(digest("recovery")), - OperatorId::new("operator-1").unwrap(), - ); - let mut consent = Consent::issue(binding.clone(), 100, 200).unwrap(); - consent.consume(&binding, 150).unwrap(); - assert!(consent.consume(&binding, 150).is_err()); - let other_operation = ConsentBinding::new( - d2b_cutover::OperationId::new("operation-2").unwrap(), - OperationKind::Cutover, - CandidateId::new("candidate-1").unwrap(), - digest("preview"), - Some(digest("recovery")), - OperatorId::new("operator-1").unwrap(), - ); - assert!(consent.consume(&other_operation, 150).is_err()); -} - -#[test] -fn reset_request_cannot_be_constructed_from_host_wide_inventory() { - let preview_inventory = inventory_in_order(false); - let preview = CutoverPreview::new_reset( - d2b_cutover::OperationId::new("reset-operation").unwrap(), - OperationKind::ScopedReset(ResetScope::Guest), - CandidateId::new("candidate-1").unwrap(), - RevisionPlanId::new("revision-1").unwrap(), - ResetInventory::new(ResetScope::Guest, "guest-1").unwrap(), - ) - .unwrap(); - let reset_inventory = ResetInventory::new(ResetScope::Guest, "guest-1").unwrap(); - let request = OperationRequest::new( - d2b_cutover::OperationId::new("reset-operation").unwrap(), - OperationKind::ScopedReset(ResetScope::Guest), - CandidateId::new("candidate-1").unwrap(), - RevisionPlanId::new("revision-1").unwrap(), - OperatorId::new("operator-1").unwrap(), - preview.digest().unwrap(), - None, - OperationInventory::Host(preview_inventory), - ); - assert!(request.is_err()); - assert!( - OperationRequest::new_reset( - d2b_cutover::OperationId::new("reset-operation").unwrap(), - ResetScope::Guest, - CandidateId::new("candidate-1").unwrap(), - RevisionPlanId::new("revision-1").unwrap(), - OperatorId::new("operator-1").unwrap(), - digest("reset-preview"), - reset_inventory, - ) - .is_ok() - ); -} - -#[test] -fn default_reset_cannot_destroy_durable_volume_without_separate_consent() { - let inventory = ResetInventory::new(ResetScope::Guest, "guest-1").unwrap(); - let operation_id = d2b_cutover::OperationId::new("reset-operation").unwrap(); - let candidate = CandidateId::new("candidate-1").unwrap(); - let revision = RevisionPlanId::new("revision-1").unwrap(); - let operator = OperatorId::new("operator-1").unwrap(); - let preview = CutoverPreview::new_reset( - operation_id.clone(), - OperationKind::ScopedReset(ResetScope::Guest), - candidate.clone(), - revision.clone(), - inventory.clone(), - ) - .unwrap(); - let request = OperationRequest::new_reset( - operation_id, - ResetScope::Guest, - candidate, - revision, - operator, - preview.digest().unwrap(), - inventory, - ) - .unwrap(); - let mut engine = ResetEngine::new(request, &preview).unwrap(); - let mut lock = HostLockContract::new(); - engine.acquire_host_lock(&mut lock).unwrap(); - let inventory_digest = match engine.request().inventory() { - d2b_cutover::OperationInventory::Reset(inventory) => inventory.digest().unwrap(), - d2b_cutover::OperationInventory::Host(_) => panic!("reset must use reset inventory"), - }; - let context = ApplyContext::reset(NOW, inventory_digest, true, true, true); - let mut consent = Consent::issue(engine.request().consent_binding(), 900, 1_900).unwrap(); - engine.begin_apply(&mut consent, &context).unwrap(); - for phase in [ - CutoverPhase::Preflight, - CutoverPhase::Consent, - CutoverPhase::Inventory, - ] { - engine - .complete_read_only_phase( - phase, - d2b_cutover::ReadOnlyEvidence { - predicates_hold: true, - audit: AuditEvidence::durable(format!("audit-{phase:?}")).unwrap(), - }, - ) - .unwrap(); - } - let destroy = EffectRequest::new( - EffectId::new("destroy-volume").unwrap(), - StepId::new("destroy-volume-step").unwrap(), - EffectKind::DestroyDurableVolume, - d2b_cutover::ReplayClass::QuarantineOnly, - None, - ); - assert_eq!( - engine.start_effect(destroy), - Err(d2b_cutover::OperationError::EffectNotAllowed( - EffectKind::DestroyDurableVolume - )) - ); - assert_eq!( - engine.state(), - OperationState::Applying(CutoverPhase::Drain) - ); -} diff --git a/packages/d2b-cutover/tests/runner_contract.rs b/packages/d2b-cutover/tests/runner_contract.rs deleted file mode 100644 index 9ee246868f..0000000000 --- a/packages/d2b-cutover/tests/runner_contract.rs +++ /dev/null @@ -1,694 +0,0 @@ -use d2b_cutover::{ - BootstrapCapability, CandidateId, CapabilityLedger, Consent, CutoverPhase, CutoverPreview, - Digest, EffectAllowlist, Journal, JournalBinding, MAX_RUNNER_FRAME_BYTES, OperationId, - OperationKind, OperationRequest, OperationState, OperatorId, ResetInventory, ResetScope, - RunnerBootstrap, RunnerCapabilityError, RunnerCommand, RunnerLockError, RunnerPaths, - RunnerSocketError, acquire_operation_lock, load_journal, persist_journal, send_command, -}; -use std::collections::BTreeSet; -use std::io::Write; -use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; -use std::os::unix::net::UnixStream; -use std::os::unix::process::CommandExt; -use std::path::Path; -use std::process::{Command, Stdio}; -use std::thread; -use std::time::Duration; - -fn digest(seed: &str) -> Digest { - Digest::derive("d2b:test:cutover-runner", seed.as_bytes()) -} - -#[test] -fn consent_digest_matches_canonical_fixture_shape() { - let json = br#"{ - "binding": { - "operationId": "op", - "operationKind": "cutover", - "candidateId": "candidate", - "previewDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "recoveryDigest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "operatorId": "operator" - }, - "issuedAtMs": 1000, - "expiresAtMs": 2000, - "consumed": false - }"#; - let consent = Consent::decode_json(json).expect("consent"); - assert_eq!( - consent.digest().expect("consent digest").as_str(), - "sha256:19a1c4f098440b052131e47edf61569f840281d1d28f7c8865292ca34b0f9f30" - ); -} - -fn ensure_scratch_root() { - std::fs::create_dir_all(".scratch").expect("create test scratch root"); -} - -#[test] -fn bootstrap_capability_is_single_use_and_rejects_replay() { - let operation_id = OperationId::new("op-runner-contract").expect("operation id"); - let candidate_id = CandidateId::new("candidate-runner-contract").expect("candidate id"); - let operator_id = OperatorId::new("operator-runner-contract").expect("operator id"); - let capability = BootstrapCapability::new( - operation_id, - candidate_id, - operator_id, - OperationKind::Cutover, - digest("nonce"), - 100, - 200, - ) - .expect("capability"); - let encoded = capability.canonical_bytes().expect("canonical capability"); - let mut ledger = CapabilityLedger::default(); - - let consumed = - BootstrapCapability::decode_and_consume(&encoded, 150, &mut ledger).expect("first use"); - assert_eq!(consumed.operation_kind(), OperationKind::Cutover); - let replay = - BootstrapCapability::decode_and_consume(&encoded, 150, &mut ledger).expect_err("replay"); - assert_eq!( - replay.to_string(), - "cutover bootstrap capability already consumed" - ); -} - -#[test] -fn bootstrap_capability_rejects_expiry_and_wrong_effect_allowlist() { - let capability = BootstrapCapability::new( - OperationId::new("op-expiry").expect("operation id"), - CandidateId::new("candidate-expiry").expect("candidate id"), - OperatorId::new("operator-expiry").expect("operator id"), - OperationKind::Cutover, - digest("expiry"), - 100, - 200, - ) - .expect("capability"); - let encoded = capability.canonical_bytes().expect("canonical capability"); - let mut ledger = CapabilityLedger::default(); - - let error = - BootstrapCapability::decode_and_consume(&encoded, 201, &mut ledger).expect_err("expired"); - assert_eq!(error.to_string(), "cutover bootstrap capability expired"); - assert_eq!( - EffectAllowlist::for_operation(OperationKind::Cutover), - EffectAllowlist::for_operation(OperationKind::Cutover) - ); -} - -#[test] -fn destructive_reset_bootstrap_requires_separate_consent() { - let operation_id = OperationId::new("op-destroy-consent").expect("operation"); - let candidate_id = CandidateId::new("candidate-destroy-consent").expect("candidate"); - let revision_plan_id = d2b_cutover::RevisionPlanId::new("plan-destroy-consent").expect("plan"); - let operator_id = OperatorId::new("operator-destroy-consent").expect("operator"); - let inventory = ResetInventory::new(ResetScope::Zone, "zone-destroy-consent") - .expect("inventory") - .with_preserve_durable_volumes(false) - .with_destroy_durable_consent(true); - let preview = CutoverPreview::new_reset( - operation_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - candidate_id.clone(), - revision_plan_id.clone(), - inventory.clone(), - ) - .expect("preview"); - let request = OperationRequest::new_reset( - operation_id.clone(), - ResetScope::Zone, - candidate_id, - revision_plan_id, - operator_id.clone(), - preview.digest().expect("preview digest"), - inventory, - ) - .expect("request"); - let capability = BootstrapCapability::new( - operation_id, - request.candidate_id().clone(), - operator_id, - OperationKind::ScopedReset(ResetScope::Zone), - digest("destroy-consent"), - 100, - 200, - ) - .expect("capability"); - let bootstrap = RunnerBootstrap { - capability, - request: request.clone(), - preview, - consent: Some(Consent::issue(request.consent_binding(), 100, 200).expect("consent")), - destructive_consent: None, - recovery: None, - host_digest: None, - }; - assert_eq!( - bootstrap.canonical_bytes().expect_err("missing consent"), - RunnerCapabilityError::Consent - ); -} - -#[test] -fn runner_socket_authority_distinguishes_admin_hold_and_owner_resume() { - ensure_scratch_root(); - let capability = BootstrapCapability::new_with_identity( - OperationId::new("op-socket-auth").expect("operation id"), - CandidateId::new("candidate-socket-auth").expect("candidate id"), - OperatorId::new("operator-socket-auth").expect("operator id"), - OperationKind::Cutover, - digest("socket-auth"), - 100, - 200, - 42, - BTreeSet::from([7]), - ) - .expect("capability"); - let bytes = capability.canonical_bytes().expect("canonical capability"); - let mut ledger = CapabilityLedger::default(); - let consumed = - BootstrapCapability::decode_and_consume(&bytes, 150, &mut ledger).expect("consume"); - let root = std::path::PathBuf::from(".scratch") - .join(format!("runner-contract-{}", std::process::id())); - let paths = RunnerPaths::new(&root, consumed.operation_id()); - let socket = d2b_cutover::RunnerSocket::bind(&paths, consumed).expect("bind socket"); - - assert!( - socket - .authorize( - 7, - &RunnerCommand::Hold { - reason: "incident".to_owned() - } - ) - .is_ok() - ); - assert_eq!( - socket - .authorize( - 7, - &RunnerCommand::Resume { - fresh_consent: None - } - ) - .expect_err("admin needs fresh consent"), - RunnerSocketError::OperatorMismatch - ); - assert!( - socket - .authorize( - 7, - &RunnerCommand::Resume { - fresh_consent: Some(digest("fresh-consent")) - } - ) - .is_ok() - ); - assert!( - socket - .authorize( - 42, - &RunnerCommand::Resume { - fresh_consent: None - } - ) - .is_ok() - ); - assert_eq!( - socket - .authorize(0, &RunnerCommand::Status) - .expect_err("HostShutdown is not a cutover peer"), - RunnerSocketError::Unauthorized - ); - assert_eq!( - socket - .authorize(8, &RunnerCommand::Status) - .expect_err("unconfigured peer"), - RunnerSocketError::Unauthorized - ); - drop(socket); - let _ = std::fs::remove_dir_all(root); -} - -#[test] -fn journal_is_root_only_and_operation_lock_is_ofd_owned() { - ensure_scratch_root(); - let operation_id = OperationId::new("op-journal-contract").expect("operation id"); - let candidate_id = CandidateId::new("candidate-journal-contract").expect("candidate id"); - let revision_plan_id = - d2b_cutover::RevisionPlanId::new("plan-journal-contract").expect("plan id"); - let operator_id = OperatorId::new("operator-journal-contract").expect("operator id"); - let inventory = - ResetInventory::new(ResetScope::Zone, "zone-journal-contract").expect("inventory"); - let preview = CutoverPreview::new_reset( - operation_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - candidate_id.clone(), - revision_plan_id.clone(), - inventory.clone(), - ) - .expect("preview"); - let preview_digest = preview.digest().expect("preview digest"); - let request = OperationRequest::new_reset( - operation_id.clone(), - ResetScope::Zone, - candidate_id.clone(), - revision_plan_id, - operator_id.clone(), - preview_digest, - inventory, - ) - .expect("request"); - let capability = BootstrapCapability::new( - operation_id.clone(), - candidate_id, - operator_id, - OperationKind::ScopedReset(ResetScope::Zone), - digest("journal-capability"), - 100, - 200, - ) - .expect("capability"); - let bootstrap = RunnerBootstrap { - capability, - request, - preview, - consent: None, - destructive_consent: None, - recovery: None, - host_digest: None, - }; - let root = std::path::PathBuf::from(".scratch") - .join(format!("journal-contract-{}", std::process::id())); - let paths = RunnerPaths::new(&root, &operation_id); - let lock = acquire_operation_lock(&paths).expect("first lock"); - let second = acquire_operation_lock(&paths).expect_err("second lock must contend"); - assert_eq!(second, RunnerLockError::Contended); - persist_journal(&paths.journal, &bootstrap, b"").expect("persist journal"); - let metadata = std::fs::metadata(&paths.journal).expect("journal metadata"); - assert_eq!( - metadata.permissions().mode() & 0o777, - 0o600, - "journal must be root-only" - ); - let (loaded, loaded_records) = load_journal(&paths.journal).expect("load journal"); - assert_eq!(loaded, bootstrap); - assert!(loaded_records.is_empty()); - drop(lock); - let _ = std::fs::remove_dir_all(root); -} - -#[test] -fn journal_persist_and_load_share_the_frame_bound() { - ensure_scratch_root(); - let operation_id = OperationId::new("op-journal-size").expect("operation id"); - let candidate_id = CandidateId::new("candidate-journal-size").expect("candidate id"); - let revision_plan_id = d2b_cutover::RevisionPlanId::new("plan-journal-size").expect("plan"); - let operator_id = OperatorId::new("operator-journal-size").expect("operator"); - let inventory = ResetInventory::new(ResetScope::Zone, "zone-journal-size").expect("inventory"); - let preview = CutoverPreview::new_reset( - operation_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - candidate_id.clone(), - revision_plan_id.clone(), - inventory.clone(), - ) - .expect("preview"); - let bootstrap = RunnerBootstrap { - capability: BootstrapCapability::new( - operation_id.clone(), - candidate_id.clone(), - operator_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - digest("journal-size"), - 100, - 200, - ) - .expect("capability"), - request: OperationRequest::new_reset( - operation_id.clone(), - ResetScope::Zone, - candidate_id, - revision_plan_id, - operator_id, - preview.digest().expect("preview digest"), - inventory, - ) - .expect("request"), - preview, - consent: None, - destructive_consent: None, - recovery: None, - host_digest: None, - }; - let root = - std::path::PathBuf::from(".scratch").join(format!("journal-size-{}", std::process::id())); - let paths = RunnerPaths::new(&root, &operation_id); - let oversized_record = format!("{{\"blob\":\"{}\"}}\n", "x".repeat(MAX_RUNNER_FRAME_BYTES)); - let persist_error = persist_journal(&paths.journal, &bootstrap, oversized_record.as_bytes()) - .expect_err("oversized journal must refuse before publication"); - assert_eq!(persist_error.kind(), std::io::ErrorKind::InvalidData); - assert!(!paths.journal.exists()); - - std::fs::create_dir_all(paths.root()).expect("journal root"); - std::fs::write(&paths.journal, vec![b'x'; MAX_RUNNER_FRAME_BYTES + 1]).expect("oversized file"); - std::fs::set_permissions(&paths.journal, std::fs::Permissions::from_mode(0o600)) - .expect("journal mode"); - let load_error = load_journal(&paths.journal).expect_err("oversized journal must refuse"); - assert_eq!(load_error.kind(), std::io::ErrorKind::InvalidData); - let _ = std::fs::remove_dir_all(root); -} - -#[test] -fn runner_binary_adopts_an_existing_journal_after_restart() { - if nix::unistd::geteuid().as_raw() != 0 { - println!("SKIP: binary runner adoption requires root"); - return; - } - let Some(runner_path) = option_env!("CARGO_BIN_EXE_d2b-cutover-runner") else { - println!("SKIP: Cargo runner binary path is unavailable"); - return; - }; - let operation_id = OperationId::new("op-binary-adopt").expect("operation"); - let candidate_id = CandidateId::new("candidate-binary-adopt").expect("candidate"); - let revision_plan_id = - d2b_cutover::RevisionPlanId::new("plan-binary-adopt").expect("revision plan"); - let operator_id = OperatorId::new("operator-binary-adopt").expect("operator"); - let inventory = ResetInventory::new(ResetScope::Zone, "zone-binary-adopt").expect("inventory"); - let preview = CutoverPreview::new_reset( - operation_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - candidate_id.clone(), - revision_plan_id.clone(), - inventory.clone(), - ) - .expect("preview"); - let bootstrap = RunnerBootstrap { - capability: BootstrapCapability::new_with_identity( - operation_id.clone(), - candidate_id.clone(), - operator_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - digest("binary-adopt"), - 100, - 200, - 0, - BTreeSet::new(), - ) - .expect("capability"), - request: OperationRequest::new_reset( - operation_id.clone(), - ResetScope::Zone, - candidate_id, - revision_plan_id.clone(), - operator_id, - preview.digest().expect("preview digest"), - inventory, - ) - .expect("request"), - preview, - consent: None, - destructive_consent: None, - recovery: None, - host_digest: None, - }; - let root = std::path::PathBuf::from(".scratch") - .join(format!("runner-binary-adopt-{}", std::process::id())); - let state_root = root.join("state"); - let socket_root = root.join("socket"); - let paths = RunnerPaths::new_with_socket_root(state_root.clone(), socket_root, &operation_id); - let mut journal = Journal::new(JournalBinding::new( - operation_id.clone(), - revision_plan_id, - bootstrap.request.request_digest().clone(), - )); - journal - .append_consent(CutoverPhase::Preflight) - .expect("consumed apply record"); - persist_journal( - &paths.journal, - &bootstrap, - &journal.to_bytes().expect("journal bytes"), - ) - .expect("persist existing journal"); - - let bootstrap_bytes = bootstrap.canonical_bytes().expect("bootstrap bytes"); - let bootstrap_path = root.join("bootstrap.json"); - std::fs::create_dir_all(&root).expect("bootstrap root"); - std::fs::write(&bootstrap_path, bootstrap_bytes).expect("write bootstrap"); - std::fs::set_permissions(&bootstrap_path, std::fs::Permissions::from_mode(0o600)) - .expect("bootstrap mode"); - let shell_path = ["/bin/sh", "/run/current-system/sw/bin/sh"] - .into_iter() - .find(|path| Path::new(path).is_file()) - .expect("shell"); - let mut child = Command::new(shell_path) - .args(["-c", "exec 3< \"$1\"; exec \"$2\"", "runner-adopt"]) - .arg(&bootstrap_path) - .arg(runner_path) - .env("D2B_CUTOVER_BOOTSTRAP_FD", "3") - .env("D2B_CUTOVER_STATE_DIR", &state_root) - .env("D2B_CUTOVER_SOCKET_DIR", root.join("socket")) - .env("D2B_BROKER_SOCKET_PATH", root.join("broker.sock")) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("spawn runner"); - let started = (0..200).any(|_| { - if let Some(status) = child.try_wait().expect("poll runner") { - panic!("runner exited before binding socket: {status}"); - } - if paths.socket.exists() { - true - } else { - thread::sleep(Duration::from_millis(10)); - false - } - }); - assert!(started, "runner socket was not created"); - let status = send_command(&paths.socket, &RunnerCommand::Status).expect("runner status"); - assert!(status.accepted); - assert_eq!( - status.status.expect("status payload").state, - OperationState::Applying(CutoverPhase::Preflight) - ); - child.kill().expect("stop runner"); - child.wait().expect("reap runner"); - let _ = std::fs::remove_dir_all(root); -} - -#[test] -fn runner_socket_survives_a_disconnected_client() { - ensure_scratch_root(); - let operator_uid = nix::unistd::geteuid().as_raw(); - let operation_id = OperationId::new("op-client-disconnect").expect("operation id"); - let capability = BootstrapCapability::new_with_identity( - operation_id.clone(), - CandidateId::new("candidate-client-disconnect").expect("candidate id"), - OperatorId::new("operator-client-disconnect").expect("operator id"), - OperationKind::ScopedReset(ResetScope::Zone), - digest("client-disconnect"), - 100, - 200, - operator_uid, - BTreeSet::new(), - ) - .expect("capability"); - let bytes = capability.canonical_bytes().expect("canonical capability"); - let mut ledger = CapabilityLedger::default(); - let consumed = - BootstrapCapability::decode_and_consume(&bytes, 150, &mut ledger).expect("consume"); - let root = std::path::PathBuf::from(".scratch") - .join(format!("runner-disconnect-{}", std::process::id())); - let paths = RunnerPaths::new(&root, &operation_id); - let socket = d2b_cutover::RunnerSocket::bind(&paths, consumed).expect("bind socket"); - let socket_path = paths.socket.clone(); - let thread = std::thread::spawn(move || { - assert!(socket.accept_command().is_err()); - let (_, command, _) = socket.accept_command().expect("second client is accepted"); - assert_eq!(command, RunnerCommand::Status); - }); - - drop(UnixStream::connect(&socket_path).expect("first client")); - let mut client = UnixStream::connect(&socket_path).expect("second client"); - client - .write_all(&serde_json::to_vec(&RunnerCommand::Status).expect("status JSON")) - .expect("write status"); - client - .shutdown(std::net::Shutdown::Write) - .expect("half-close status"); - thread.join().expect("runner listener thread"); - let _ = std::fs::remove_dir_all(root); -} - -#[test] -fn runner_socket_refuses_a_foreign_socket_directory_link() { - let operation_id = OperationId::new("op-foreign-socket").expect("operation id"); - let capability = BootstrapCapability::new( - operation_id.clone(), - CandidateId::new("candidate-foreign-socket").expect("candidate id"), - OperatorId::new("operator-foreign-socket").expect("operator id"), - OperationKind::ScopedReset(ResetScope::Zone), - digest("foreign-socket"), - 100, - 200, - ) - .expect("capability"); - let bytes = capability.canonical_bytes().expect("canonical capability"); - let mut ledger = CapabilityLedger::default(); - let consumed = - BootstrapCapability::decode_and_consume(&bytes, 150, &mut ledger).expect("consume"); - let root = - std::path::PathBuf::from(".scratch").join(format!("runner-foreign-{}", std::process::id())); - let state_root = root.join("state"); - let socket_root = root.join("socket"); - std::fs::create_dir_all(&socket_root).expect("socket root"); - let target = root.join("foreign-target"); - std::fs::create_dir_all(&target).expect("foreign target"); - std::os::unix::fs::symlink(&target, socket_root.join("cutover")).expect("foreign link"); - let paths = RunnerPaths::new_with_socket_root(state_root, socket_root, &operation_id); - let error = match d2b_cutover::RunnerSocket::bind(&paths, consumed) { - Ok(_) => panic!("foreign socket directory must refuse"), - Err(error) => error, - }; - assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied); - let _ = std::fs::remove_dir_all(root); -} - -#[test] -fn lifecycle_group_traverses_runtime_socket_without_state_access() { - let root_uid = nix::unistd::geteuid().as_raw(); - if root_uid != 0 { - println!("SKIP: runtime socket ownership contract requires root"); - return; - } - let lifecycle_gid = nix::unistd::getgid().as_raw(); - let nonmember_gid = if lifecycle_gid == 65_534 { - 65_533 - } else { - 65_534 - }; - let operation_id = OperationId::new("op-socket-group").expect("operation id"); - let candidate_id = CandidateId::new("candidate-socket-group").expect("candidate id"); - let revision_plan_id = - d2b_cutover::RevisionPlanId::new("plan-socket-group").expect("revision plan id"); - let operator_id = OperatorId::new(format!("uid-{root_uid}")).expect("operator id"); - let inventory = ResetInventory::new(ResetScope::Zone, "zone-socket-group").expect("inventory"); - let preview = CutoverPreview::new_reset( - operation_id.clone(), - OperationKind::ScopedReset(ResetScope::Zone), - candidate_id.clone(), - revision_plan_id.clone(), - inventory.clone(), - ) - .expect("preview"); - let request = OperationRequest::new_reset( - operation_id.clone(), - ResetScope::Zone, - candidate_id.clone(), - revision_plan_id, - operator_id.clone(), - preview.digest().expect("preview digest"), - inventory, - ) - .expect("request"); - let capability = BootstrapCapability::new_with_identity_and_group( - operation_id.clone(), - candidate_id, - operator_id, - OperationKind::ScopedReset(ResetScope::Zone), - digest("socket-group"), - 100, - 200, - root_uid, - BTreeSet::new(), - Some(lifecycle_gid), - ) - .expect("capability"); - let capability_bytes = capability.canonical_bytes().expect("capability bytes"); - let mut ledger = CapabilityLedger::default(); - let consumed = BootstrapCapability::decode_and_consume(&capability_bytes, 150, &mut ledger) - .expect("consume capability"); - let root = Path::new(".scratch").join(format!("runner-socket-group-{}", std::process::id())); - let state_root = root.join("state"); - let socket_root = root.join("runtime"); - std::fs::create_dir_all(&socket_root).expect("socket root"); - std::fs::set_permissions(&socket_root, std::fs::Permissions::from_mode(0o755)) - .expect("socket root mode"); - let paths = RunnerPaths::new_with_socket_root(state_root, socket_root, &operation_id); - let socket = d2b_cutover::RunnerSocket::bind(&paths, consumed).expect("bind socket"); - let bootstrap = RunnerBootstrap { - capability, - request, - preview, - consent: None, - destructive_consent: None, - recovery: None, - host_digest: None, - }; - persist_journal(&paths.journal, &bootstrap, b"").expect("persist journal"); - - let shared_socket_dir = paths - .socket_dir() - .parent() - .expect("shared socket directory"); - for directory in [shared_socket_dir, paths.socket_dir()] { - let metadata = std::fs::symlink_metadata(directory).expect("socket directory metadata"); - assert!(metadata.is_dir()); - assert_eq!(metadata.uid(), 0); - assert_eq!(metadata.gid(), lifecycle_gid); - assert_eq!(metadata.permissions().mode() & 0o777, 0o710); - } - let socket_metadata = std::fs::symlink_metadata(&paths.socket).expect("socket metadata"); - assert!(socket_metadata.file_type().is_socket()); - assert_eq!(socket_metadata.uid(), 0); - assert_eq!(socket_metadata.gid(), lifecycle_gid); - assert_eq!(socket_metadata.permissions().mode() & 0o777, 0o660); - let state_metadata = std::fs::symlink_metadata(paths.operation_dir()).expect("state metadata"); - assert_eq!(state_metadata.uid(), 0); - assert_eq!(state_metadata.permissions().mode() & 0o777, 0o700); - let journal_metadata = std::fs::symlink_metadata(&paths.journal).expect("journal metadata"); - assert_eq!(journal_metadata.uid(), 0); - assert_eq!(journal_metadata.permissions().mode() & 0o777, 0o600); - - run_socket_probe(&paths.socket, lifecycle_gid, "connect", true); - run_socket_probe(&paths.socket, nonmember_gid, "connect", false); - run_socket_probe(&paths.journal, lifecycle_gid, "read", false); - - drop(socket); - let _ = std::fs::remove_dir_all(root); -} - -#[test] -fn socket_probe_child() { - let Ok(mode) = std::env::var("D2B_SOCKET_PROBE_MODE") else { - return; - }; - let path = std::env::var("D2B_SOCKET_PROBE_PATH").expect("probe path"); - let expected = std::env::var("D2B_SOCKET_PROBE_EXPECTED").expect("probe expectation"); - let actual = match mode.as_str() { - "connect" => UnixStream::connect(path).is_ok(), - "read" => std::fs::read(path).is_ok(), - other => panic!("unknown probe mode {other}"), - }; - assert_eq!(actual, expected == "yes"); -} - -fn run_socket_probe(path: &Path, gid: u32, mode: &str, expected: bool) { - let status = Command::new(std::env::current_exe().expect("test executable")) - .args(["--exact", "socket_probe_child", "--nocapture"]) - .env("D2B_SOCKET_PROBE_MODE", mode) - .env("D2B_SOCKET_PROBE_PATH", path) - .env( - "D2B_SOCKET_PROBE_EXPECTED", - if expected { "yes" } else { "no" }, - ) - .uid(65_534) - .gid(gid) - .status() - .expect("run socket probe"); - assert!(status.success(), "socket probe failed: {status}"); -} diff --git a/packages/d2b-cutover/tests/state_machine.rs b/packages/d2b-cutover/tests/state_machine.rs deleted file mode 100644 index fede582c7e..0000000000 --- a/packages/d2b-cutover/tests/state_machine.rs +++ /dev/null @@ -1,557 +0,0 @@ -use d2b_cutover::{ - ApplyContext, ArtifactId, AuditEvidence, CandidateId, CompletionEvidence, Consent, - CutoverEngine, CutoverPhase, CutoverPreview, Digest, Disposition, EffectEvidence, EffectId, - EffectKind, EffectRequest, FailureCode, FinalizationConsent, HoldReason, HostInventory, - HostLockContract, InventoryClass, InventoryItem, OperationRequest, OperationState, OperatorId, - RecoveryAttestation, RecoveryId, ReplayClass, RevisionPlanId, StepId, VerificationInput, - ZoneId, ZoneInventory, ZoneVerification, -}; - -const NOW: u64 = 1_000; - -fn digest(label: &str) -> Digest { - Digest::derive("d2b:test", label.as_bytes()) -} - -fn id(value: &str, constructor: impl FnOnce(String) -> Result) -> T { - constructor(value.to_owned()).unwrap() -} - -fn inventory() -> HostInventory { - let zone_a = ZoneInventory::new( - "zone-a", - true, - [InventoryItem::classified( - "zone-a-tpm", - InventoryClass::TpmIdentity, - Disposition::Adopt, - ) - .unwrap() - .into()], - ) - .unwrap(); - let zone_b = ZoneInventory::new( - "zone-b", - true, - [InventoryItem::unclassified("zone-b-unknown") - .unwrap() - .into()], - ) - .unwrap(); - HostInventory::build( - [ - ZoneId::new("zone-b").unwrap(), - ZoneId::new("zone-a").unwrap(), - ], - [zone_b, zone_a], - [InventoryItem::classified( - "host-volume", - InventoryClass::DurableVolume, - Disposition::Adopt, - ) - .unwrap() - .into()], - ) - .unwrap() -} - -struct Fixture { - engine: CutoverEngine, - context: ApplyContext, -} - -fn fixture() -> Fixture { - let inventory = inventory(); - let inventory_digest = inventory.digest().unwrap(); - let operation_id = id("operation-1", d2b_cutover::OperationId::new); - let candidate_id = id("candidate-1", CandidateId::new); - let revision = id("revision-1", RevisionPlanId::new); - let operator = id("operator-1", OperatorId::new); - let preview = CutoverPreview::new( - operation_id.clone(), - d2b_cutover::OperationKind::Cutover, - candidate_id.clone(), - revision.clone(), - inventory.clone(), - None, - ) - .unwrap(); - let preview_digest = preview.digest().unwrap(); - let recovery = RecoveryAttestation::new( - id("recovery-1", RecoveryId::new), - candidate_id.clone(), - digest("host"), - preview_digest.clone(), - operator.clone(), - digest("restore"), - 900, - 1_900, - true, - ) - .unwrap(); - let recovery_digest = recovery.digest().unwrap(); - let request = OperationRequest::new_cutover( - operation_id, - candidate_id, - revision, - operator, - preview_digest, - recovery_digest, - inventory, - ) - .unwrap(); - let mut engine = CutoverEngine::new(request, &preview).unwrap(); - let mut lock = HostLockContract::new(); - engine.acquire_host_lock(&mut lock).unwrap(); - let context = ApplyContext::cutover( - NOW, - inventory_digest.clone(), - true, - true, - true, - recovery, - digest("host"), - ); - let mut consent = Consent::issue(engine.request().consent_binding(), 900, 1_900).unwrap(); - engine.begin_apply(&mut consent, &context).unwrap(); - for phase in [ - CutoverPhase::Preflight, - CutoverPhase::Consent, - CutoverPhase::Inventory, - ] { - engine - .complete_read_only_phase( - phase, - d2b_cutover::ReadOnlyEvidence { - predicates_hold: true, - audit: AuditEvidence::durable(format!("audit-{phase:?}")).unwrap(), - }, - ) - .unwrap(); - } - Fixture { engine, context } -} - -fn effect( - number: &str, - kind: EffectKind, - replay: ReplayClass, - advance_to: Option, -) -> EffectRequest { - EffectRequest::new( - EffectId::new(format!("effect-{number}")).unwrap(), - StepId::new(format!("step-{number}")).unwrap(), - kind, - replay, - advance_to, - ) -} - -fn complete(engine: &mut CutoverEngine, request: &EffectRequest) { - engine - .complete_effect( - request.effect_id(), - CompletionEvidence { - effect: EffectEvidence::succeeded(), - audit: AuditEvidence::durable(format!("audit-{}", request.effect_id())).unwrap(), - }, - ) - .unwrap(); -} - -#[test] -fn u3_exposes_closed_phase_and_operation_contracts() { - assert_eq!(CutoverPhase::Preflight.number(), 0); - assert_eq!(CutoverPhase::Finalization.number(), 10); -} - -#[test] -fn all_zone_inventory_is_sorted_and_unknown_items_preserve() { - let inventory = inventory(); - assert_eq!( - inventory.zone_ids().map(ZoneId::as_str).collect::>(), - ["zone-a", "zone-b"] - ); - assert_eq!( - inventory - .zones() - .iter() - .find(|zone| zone.zone_id().as_str() == "zone-b") - .unwrap() - .items()[0] - .disposition(), - Disposition::Preserve - ); - assert!(inventory.sources_retained()); -} - -#[test] -fn lock_contention_refuses_the_second_operation_without_mutation() { - let fixture = fixture(); - let mut first_lock = HostLockContract::new(); - let mut first = fixture.engine; - first.acquire_host_lock(&mut first_lock).unwrap(); - - let inventory = inventory(); - let preview = CutoverPreview::new( - d2b_cutover::OperationId::new("operation-2").unwrap(), - d2b_cutover::OperationKind::Cutover, - CandidateId::new("candidate-2").unwrap(), - RevisionPlanId::new("revision-2").unwrap(), - inventory.clone(), - None, - ) - .unwrap(); - let request = OperationRequest::new( - d2b_cutover::OperationId::new("operation-2").unwrap(), - d2b_cutover::OperationKind::Cutover, - CandidateId::new("candidate-2").unwrap(), - RevisionPlanId::new("revision-2").unwrap(), - OperatorId::new("operator-2").unwrap(), - preview.digest().unwrap(), - Some(digest("recovery-2")), - d2b_cutover::OperationInventory::Host(inventory), - ) - .unwrap(); - let mut second = CutoverEngine::new(request, &preview).unwrap(); - assert!(matches!( - second.acquire_host_lock(&mut first_lock), - Err(d2b_cutover::OperationError::LockContended(_)) - )); - assert!(second.journal().records().is_empty()); - assert!(first.current_effect().is_none()); -} - -#[test] -fn hold_waits_for_current_atomic_step_and_resume_reuses_operation() { - let mut fixture = fixture(); - let request = effect( - "drain", - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - fixture.engine.start_effect(request.clone()).unwrap(); - fixture - .engine - .request_hold( - OperatorId::new("other-admin").unwrap(), - HoldReason::new("pause for inspection").unwrap(), - AuditEvidence::durable("hold-request").unwrap(), - ) - .unwrap(); - assert!(!fixture.engine.hold().is_active()); - complete(&mut fixture.engine, &request); - assert_eq!(fixture.engine.state(), OperationState::Held); - let bound_operator = fixture.engine.request().operator_id().clone(); - fixture - .engine - .resume( - &bound_operator, - &fixture.context, - AuditEvidence::durable("hold-clear").unwrap(), - ) - .unwrap(); - assert_eq!( - fixture.engine.state(), - OperationState::Applying(CutoverPhase::Disposition) - ); -} - -#[test] -fn audit_failure_does_not_advance_or_clear_started_effect() { - let mut fixture = fixture(); - let request = effect( - "drain", - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - fixture.engine.start_effect(request.clone()).unwrap(); - assert_eq!( - fixture.engine.complete_effect( - request.effect_id(), - CompletionEvidence { - effect: EffectEvidence::succeeded(), - audit: AuditEvidence::unavailable(), - }, - ), - Err(d2b_cutover::OperationError::AuditNotDurable) - ); - assert_eq!(fixture.engine.phase(), CutoverPhase::Drain); - assert!(fixture.engine.current_effect().is_some()); -} - -#[test] -fn phase_four_rollback_preserves_sources_and_quarantines_staged_destination() { - let mut fixture = fixture(); - let drain = effect( - "drain", - EffectKind::HostDrain, - ReplayClass::Repeatable, - Some(CutoverPhase::Disposition), - ); - fixture.engine.start_effect(drain.clone()).unwrap(); - complete(&mut fixture.engine, &drain); - let disposition = effect( - "disposition", - EffectKind::CutoverDisposition, - ReplayClass::ReopenByJournaledIdentity, - None, - ) - .with_identity( - Some(ArtifactId::new("journaled-store").unwrap()), - Some(ArtifactId::new("staged-store").unwrap()), - ); - fixture.engine.start_effect(disposition.clone()).unwrap(); - fixture - .engine - .complete_effect( - disposition.effect_id(), - CompletionEvidence { - effect: EffectEvidence::succeeded_with_identity("journaled-store").unwrap(), - audit: AuditEvidence::durable("disposition-audit").unwrap(), - }, - ) - .unwrap(); - let rollback = fixture - .engine - .rollback(AuditEvidence::durable("rollback-audit").unwrap()) - .unwrap(); - assert!(rollback.sources_preserved()); - assert_eq!( - rollback.quarantined_destinations(), - &[ArtifactId::new("staged-store").unwrap()] - ); - assert_eq!(fixture.engine.state(), OperationState::RolledBack); -} - -#[test] -fn phase_five_rollback_is_refused() { - let mut fixture = fixture(); - let request = effect( - "store", - EffectKind::ResourceStoreCreate, - ReplayClass::ReopenByJournaledIdentity, - Some(CutoverPhase::ProviderInstall), - ) - .with_identity( - Some(ArtifactId::new("store-uuid").unwrap()), - Some(ArtifactId::new("store-destination").unwrap()), - ); - fixture.engine.start_effect(request.clone()).unwrap(); - fixture - .engine - .complete_effect( - request.effect_id(), - CompletionEvidence { - effect: EffectEvidence::succeeded_with_identity("store-uuid").unwrap(), - audit: AuditEvidence::durable("store-audit").unwrap(), - }, - ) - .unwrap(); - assert!(matches!( - fixture - .engine - .rollback(AuditEvidence::durable("rollback-audit").unwrap()), - Err(d2b_cutover::OperationError::Rollback( - d2b_cutover::RollbackError::BoundaryClosed(CutoverPhase::ProviderInstall) - )) - )); -} - -#[test] -fn terminal_outcome_is_write_once() { - let mut fixture = fixture(); - fixture - .engine - .fail_terminal( - FailureCode::EffectFailed, - AuditEvidence::durable("failure-audit").unwrap(), - ) - .unwrap(); - assert_eq!(fixture.engine.state(), OperationState::Failed); - assert_eq!( - fixture.engine.fail_terminal( - FailureCode::CandidateDrift, - AuditEvidence::durable("second").unwrap(), - ), - Err(d2b_cutover::OperationError::TerminalAlreadyWritten) - ); -} - -#[test] -fn phase_ten_requires_distinct_consent_and_closes_once() { - let mut fixture = fixture(); - for ((number, kind), next) in [ - (("drain", EffectKind::HostDrain), CutoverPhase::Disposition), - ( - ("disposition", EffectKind::CutoverDisposition), - CutoverPhase::ResourceStore, - ), - ( - ("store", EffectKind::ResourceStoreCreate), - CutoverPhase::ProviderInstall, - ), - ( - ("provider", EffectKind::ProviderInstall), - CutoverPhase::ZoneCutover, - ), - ( - ("zone", EffectKind::ZoneActivation), - CutoverPhase::Activation, - ), - ( - ("guest", EffectKind::GuestActivation), - CutoverPhase::Verification, - ), - ] { - let request = effect( - number, - kind, - if kind == EffectKind::ResourceStoreCreate { - ReplayClass::ReopenByJournaledIdentity - } else { - ReplayClass::Repeatable - }, - Some(next), - ); - let request = if kind == EffectKind::ResourceStoreCreate { - request.with_identity( - Some(ArtifactId::new("store-uuid").unwrap()), - Some(ArtifactId::new("store-destination").unwrap()), - ) - } else { - request - }; - fixture.engine.start_effect(request.clone()).unwrap(); - let evidence = if request.identity_bearing() { - EffectEvidence::succeeded_with_identity("store-uuid").unwrap() - } else { - EffectEvidence::succeeded() - }; - fixture - .engine - .complete_effect( - request.effect_id(), - CompletionEvidence { - effect: evidence, - audit: AuditEvidence::durable(format!("audit-{number}")).unwrap(), - }, - ) - .unwrap(); - } - fixture - .engine - .verify(&VerificationInput::new( - [ - ZoneVerification::new(ZoneId::new("zone-a").unwrap(), true), - ZoneVerification::new(ZoneId::new("zone-b").unwrap(), true), - ], - true, - true, - true, - true, - )) - .unwrap(); - let mut finalization = FinalizationConsent::issue( - fixture.engine.request().finalization_binding(), - NOW, - NOW + 500, - ) - .unwrap(); - fixture - .engine - .begin_finalization(&mut finalization, NOW + 1) - .unwrap(); - let request = effect( - "finalize", - EffectKind::CutoverFinalization, - ReplayClass::QuarantineOnly, - None, - ); - fixture.engine.start_effect(request.clone()).unwrap(); - complete(&mut fixture.engine, &request); - assert_eq!(fixture.engine.state(), OperationState::Closed); - assert_eq!( - fixture.engine.terminal_outcome(), - Some(d2b_cutover::TerminalOutcomeKind::Closed) - ); -} - -#[test] -fn verification_requires_every_zone_and_preserved_identity_before_success() { - let mut fixture = fixture(); - for ((number, kind), next) in [ - (("drain", EffectKind::HostDrain), CutoverPhase::Disposition), - ( - ("disposition", EffectKind::CutoverDisposition), - CutoverPhase::ResourceStore, - ), - ( - ("store", EffectKind::ResourceStoreCreate), - CutoverPhase::ProviderInstall, - ), - ( - ("provider", EffectKind::ProviderInstall), - CutoverPhase::ZoneCutover, - ), - ( - ("zone", EffectKind::ZoneActivation), - CutoverPhase::Activation, - ), - ( - ("guest", EffectKind::GuestActivation), - CutoverPhase::Verification, - ), - ] { - let request = effect( - number, - kind, - if kind == EffectKind::ResourceStoreCreate { - ReplayClass::ReopenByJournaledIdentity - } else { - ReplayClass::Repeatable - }, - Some(next), - ); - let request = if kind == EffectKind::ResourceStoreCreate { - request.with_identity( - Some(ArtifactId::new("store-uuid").unwrap()), - Some(ArtifactId::new("store-destination").unwrap()), - ) - } else { - request - }; - fixture.engine.start_effect(request.clone()).unwrap(); - if request.identity_bearing() { - fixture - .engine - .complete_effect( - request.effect_id(), - CompletionEvidence { - effect: EffectEvidence::succeeded_with_identity("store-uuid").unwrap(), - audit: AuditEvidence::durable(format!("audit-{number}")).unwrap(), - }, - ) - .unwrap(); - } else { - complete(&mut fixture.engine, &request); - } - } - let report = fixture - .engine - .verify(&VerificationInput::new( - [ - ZoneVerification::new(ZoneId::new("zone-a").unwrap(), true), - ZoneVerification::new(ZoneId::new("zone-b").unwrap(), true), - ], - true, - true, - true, - true, - )) - .unwrap(); - assert_eq!(report.zone_count(), 2); - assert_eq!(fixture.engine.state(), OperationState::CutoverSucceeded); -} diff --git a/packages/d2b-exec-runner/BUILD.bazel b/packages/d2b-exec-runner/BUILD.bazel deleted file mode 100644 index 8c6653ccc7..0000000000 --- a/packages/d2b-exec-runner/BUILD.bazel +++ /dev/null @@ -1,92 +0,0 @@ -load("@crates//:defs.bzl", "all_crate_deps") -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test", "rust_doc_test") - -exports_files( - ["BUILD.bazel"], - visibility = ["//visibility:public"], -) - -rust_library( - name = "d2b_exec_runner", - srcs = glob(["src/**/*.rs"], allow_empty = True), - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], -) - -rust_binary( - name = "d2b-exec-runner", - srcs = glob(["src/**/*.rs"], allow_empty = True), - crate_root = "src/main.rs", - compile_data = ["Cargo.toml"], - visibility = ["//visibility:public"], - deps = [ - ":d2b_exec_runner", - ] + all_crate_deps(normal = True, cargo_only = True), -) - -rust_test( - name = "fail_closed", - srcs = ["tests/fail_closed.rs"], - compile_data = ["Cargo.toml"], - data = [":d2b-exec-runner"], - env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, - visibility = ["//visibility:public"], -) - -rust_test( - name = "tty_pty_integration", - srcs = ["tests/tty_pty_integration.rs"], - compile_data = ["Cargo.toml"], - data = [":d2b-exec-runner"], - env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, - visibility = ["//visibility:public"], - deps = [] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - -rust_test( - name = "d2b_exec_runner_test", - compile_data = ["Cargo.toml"], - crate = ":d2b_exec_runner", - data = [":d2b-exec-runner"], - env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, -) - -rust_test( - name = "d2b-exec-runner_test", - compile_data = ["Cargo.toml"], - crate = ":d2b-exec-runner", - data = [":d2b-exec-runner"], - env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-exec-runner": "$(rootpath :d2b-exec-runner)"}, -) - -# keep -filegroup( - name = "cargo_workspace_sources", - srcs = glob( - [ - "Cargo.lock", - "Cargo.toml", - "benches/**/*.rs", - "build.rs", - "examples/**/*.rs", - "src/**/*.rs", - "tests/**/*.rs", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) - -rust_doc_test( - name = "d2b_exec_runner_doc_test", - crate = "//packages/d2b-exec-runner:d2b_exec_runner", -) - -test_suite( - name = "all-tests", - tags = ["-local", "-no-remote-exec"], -) diff --git a/packages/d2b-exec-runner/Cargo.toml b/packages/d2b-exec-runner/Cargo.toml deleted file mode 100644 index 77bad211a2..0000000000 --- a/packages/d2b-exec-runner/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "d2b-exec-runner" -version = "0.0.0-bootstrap" -edition = "2024" -publish = false -license.workspace = true - -[lib] -name = "d2b_exec_runner" -path = "src/lib.rs" - -[[bin]] -name = "d2b-exec-runner" -path = "src/main.rs" - -[lints] -workspace = true - -# The library half stays dependency-pure (std only) so guestd can depend on -# it as the FileRing/SpecCodec/record contract oracle without dragging in any -# native-link dependency. `rustix` is consumed by the binary half only (slot -# dir-fd openat, process-group signalling) and is already proven static in the -# guest dependency-policy gate. -[dependencies] -rustix = { workspace = true } - diff --git a/packages/d2b-exec-runner/src/atomicio.rs b/packages/d2b-exec-runner/src/atomicio.rs deleted file mode 100644 index e39c000533..0000000000 --- a/packages/d2b-exec-runner/src/atomicio.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Shared, dependency-pure atomic file I/O primitives. -//! -//! Both the FileRing sidecar and the runner's `status`/`record`/`cancel` -//! writers need the same durable-replace contract: write a temp file in the -//! same directory, fsync it, rename it over the target, then fsync the parent -//! directory so the rename itself is durable. Symlink-safe opens use -//! `O_NOFOLLOW` on the final path component. - -use std::fs::OpenOptions; -use std::io::{self, Read}; -use std::os::unix::fs::{FileExt, OpenOptionsExt}; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; - -/// `O_NOFOLLOW` for Linux (the only guest target). Octal `0o400000`; identical -/// on x86_64 and aarch64. -#[cfg(target_os = "linux")] -pub(crate) const O_NOFOLLOW: i32 = 0o0_400_000; -#[cfg(not(target_os = "linux"))] -pub(crate) const O_NOFOLLOW: i32 = 0; - -/// Every per-slot file holds privileged exec metadata/output; create it -/// `root:root 0600` explicitly rather than relying on the process umask. -pub(crate) const FILE_MODE_0600: u32 = 0o600; - -/// Monotonic counter making each atomic-write temp name unique within a -/// process, so two concurrent writers to the same target (e.g. two concurrent -/// `ExecCancel`s racing the `cancel` sentinel) never collide on a shared -/// `.tmp` file. -static TMP_SEQ: AtomicU64 = AtomicU64::new(0); - -/// Open a file for reading, refusing to follow a final-component symlink. -pub fn open_read_nofollow(path: &Path) -> io::Result { - OpenOptions::new() - .read(true) - .custom_flags(O_NOFOLLOW) - .open(path) -} - -/// Read an entire small control file (status/record/sidecar/spec). The caller -/// is responsible for any size validation via the decoder's bounded fields. -pub fn read_file_nofollow(path: &Path) -> io::Result> { - let mut file = open_read_nofollow(path)?; - let mut buf = Vec::new(); - file.read_to_end(&mut buf)?; - Ok(buf) -} - -/// fsync the directory that contains `path` so a rename is durable. -pub fn fsync_parent_dir(path: &Path) -> io::Result<()> { - let dir = path.parent().unwrap_or_else(|| Path::new(".")); - let handle = OpenOptions::new() - .read(true) - .custom_flags(O_NOFOLLOW) - .open(dir)?; - handle.sync_all() -} - -fn tmp_path(target: &Path) -> PathBuf { - let mut name = target - .file_name() - .map(|n| n.to_os_string()) - .unwrap_or_default(); - // Unique per write: `.` so concurrent writers never share a temp. - let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed); - name.push(format!(".tmp.{}.{seq}", std::process::id())); - target.with_file_name(name) -} - -/// Atomically replace `target` with `bytes` (temp -> fsync -> rename -> dir -/// fsync). The temp file is created `O_NOFOLLOW|O_CREAT|O_EXCL` with an -/// explicit `0600` mode (never umask-derived) and a unique name; any failure -/// after creation removes the temp so a dirty slot is never left behind. -pub fn atomic_write(target: &Path, bytes: &[u8]) -> io::Result<()> { - let tmp = tmp_path(target); - let write_result = (|| { - let tmp_file = OpenOptions::new() - .create_new(true) - .write(true) - .mode(FILE_MODE_0600) - .custom_flags(O_NOFOLLOW) - .open(&tmp)?; - tmp_file.write_all_at(bytes, 0)?; - tmp_file.set_len(bytes.len() as u64)?; - tmp_file.sync_all()?; - std::fs::rename(&tmp, target)?; - fsync_parent_dir(target) - })(); - if write_result.is_err() { - // Best-effort: the rename consumes `tmp` on success, so a leftover only - // exists on the error path. - let _ = std::fs::remove_file(&tmp); - } - write_result -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::Arc; - - fn scratch() -> PathBuf { - let base = std::env::var_os("TMPDIR") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(".")); - let dir = base.join(format!( - "atomicio-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - std::fs::create_dir_all(&dir).unwrap(); - dir - } - - #[test] - fn atomic_write_then_read_round_trips_and_replaces() { - let dir = scratch(); - let target = dir.join("status"); - atomic_write(&target, b"first").unwrap(); - assert_eq!(read_file_nofollow(&target).unwrap(), b"first"); - // Replace with a shorter payload; no stale tail remains. - atomic_write(&target, b"hi").unwrap(); - assert_eq!(read_file_nofollow(&target).unwrap(), b"hi"); - // The temp file is gone after a successful rename (unique-named, so we - // assert no `.tmp.*` siblings linger). - let leftover = std::fs::read_dir(&dir) - .unwrap() - .flatten() - .any(|e| e.file_name().to_string_lossy().contains(".tmp.")); - assert!(!leftover, "no temp file lingers after a successful write"); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn atomic_write_creates_files_mode_0600() { - use std::os::unix::fs::MetadataExt; - let dir = scratch(); - let target = dir.join("record"); - atomic_write(&target, b"payload").unwrap(); - let mode = std::fs::metadata(&target).unwrap().mode() & 0o777; - assert_eq!( - mode, 0o600, - "data files are created 0600, not umask-derived" - ); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn concurrent_writes_to_same_target_never_collide_on_temp() { - // Two threads writing the same target concurrently must both succeed: - // the temp name is unique per write, so neither clobbers the other's - // temp (the concurrent-ExecCancel `cancel.tmp` collision). - let dir = scratch(); - let target = Arc::new(dir.join("cancel")); - let mut handles = Vec::new(); - for _ in 0..8 { - let target = Arc::clone(&target); - handles.push(std::thread::spawn(move || { - for _ in 0..50 { - atomic_write(&target, b"1").expect("concurrent atomic_write"); - } - })); - } - for h in handles { - h.join().unwrap(); - } - assert_eq!(read_file_nofollow(&target).unwrap(), b"1"); - let leftover = std::fs::read_dir(&dir) - .unwrap() - .flatten() - .any(|e| e.file_name().to_string_lossy().contains(".tmp.")); - assert!(!leftover, "no temp files linger after concurrent writes"); - std::fs::remove_dir_all(&dir).ok(); - } -} diff --git a/packages/d2b-exec-runner/src/codec.rs b/packages/d2b-exec-runner/src/codec.rs deleted file mode 100644 index bd3583fd6d..0000000000 --- a/packages/d2b-exec-runner/src/codec.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! Minimal length-prefixed binary codec primitives (no serde). -//! -//! All multi-byte integers are little-endian. Strings and byte blobs are -//! `u32` length-prefixed. Byte budgets are validated on decode so a corrupt -//! or hostile on-disk file can never drive an unbounded allocation. - -/// Hard ceiling for any single length-prefixed field on decode. Generous -/// enough for the largest validated argv/env value, small enough to refuse a -/// corrupt length word. -pub const MAX_FIELD_BYTES: usize = 1 << 20; - -/// Append-only byte writer. -#[derive(Default)] -pub struct Writer { - buf: Vec, -} - -impl Writer { - pub fn new() -> Self { - Self { buf: Vec::new() } - } - - pub fn put_u8(&mut self, value: u8) { - self.buf.push(value); - } - - pub fn put_u32(&mut self, value: u32) { - self.buf.extend_from_slice(&value.to_le_bytes()); - } - - pub fn put_u64(&mut self, value: u64) { - self.buf.extend_from_slice(&value.to_le_bytes()); - } - - pub fn put_i32(&mut self, value: i32) { - self.buf.extend_from_slice(&value.to_le_bytes()); - } - - pub fn put_bool(&mut self, value: bool) { - self.buf.push(u8::from(value)); - } - - pub fn put_bytes(&mut self, bytes: &[u8]) { - self.put_u32(bytes.len() as u32); - self.buf.extend_from_slice(bytes); - } - - pub fn put_str(&mut self, value: &str) { - self.put_bytes(value.as_bytes()); - } - - pub fn into_vec(self) -> Vec { - self.buf - } -} - -/// Typed decode failure. Carries no payload bytes. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DecodeError { - Truncated, - FieldTooLong, - InvalidUtf8, - InvalidTag, - TrailingBytes, - BadMagic, - BadVersion, -} - -/// Cursor reader over a byte slice. -pub struct Reader<'a> { - data: &'a [u8], - pos: usize, -} - -impl<'a> Reader<'a> { - pub fn new(data: &'a [u8]) -> Self { - Self { data, pos: 0 } - } - - fn take(&mut self, n: usize) -> Result<&'a [u8], DecodeError> { - let end = self.pos.checked_add(n).ok_or(DecodeError::Truncated)?; - if end > self.data.len() { - return Err(DecodeError::Truncated); - } - let slice = &self.data[self.pos..end]; - self.pos = end; - Ok(slice) - } - - pub fn get_u8(&mut self) -> Result { - Ok(self.take(1)?[0]) - } - - pub fn get_u32(&mut self) -> Result { - let bytes = self.take(4)?; - Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) - } - - pub fn get_u64(&mut self) -> Result { - let bytes = self.take(8)?; - let mut buf = [0u8; 8]; - buf.copy_from_slice(bytes); - Ok(u64::from_le_bytes(buf)) - } - - pub fn get_i32(&mut self) -> Result { - let bytes = self.take(4)?; - Ok(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) - } - - pub fn get_bool(&mut self) -> Result { - match self.get_u8()? { - 0 => Ok(false), - 1 => Ok(true), - _ => Err(DecodeError::InvalidTag), - } - } - - pub fn get_bytes(&mut self) -> Result, DecodeError> { - let len = self.get_u32()? as usize; - if len > MAX_FIELD_BYTES { - return Err(DecodeError::FieldTooLong); - } - Ok(self.take(len)?.to_vec()) - } - - pub fn get_str(&mut self) -> Result { - let bytes = self.get_bytes()?; - String::from_utf8(bytes).map_err(|_| DecodeError::InvalidUtf8) - } - - /// Ensure every byte was consumed; reject trailing garbage. - pub fn finish(self) -> Result<(), DecodeError> { - if self.pos == self.data.len() { - Ok(()) - } else { - Err(DecodeError::TrailingBytes) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn round_trips_primitives() { - let mut w = Writer::new(); - w.put_u8(7); - w.put_u32(42); - w.put_u64(1 << 40); - w.put_i32(-9); - w.put_bool(true); - w.put_str("hello"); - w.put_bytes(&[1, 2, 3]); - let bytes = w.into_vec(); - - let mut r = Reader::new(&bytes); - assert_eq!(r.get_u8().unwrap(), 7); - assert_eq!(r.get_u32().unwrap(), 42); - assert_eq!(r.get_u64().unwrap(), 1 << 40); - assert_eq!(r.get_i32().unwrap(), -9); - assert!(r.get_bool().unwrap()); - assert_eq!(r.get_str().unwrap(), "hello"); - assert_eq!(r.get_bytes().unwrap(), vec![1, 2, 3]); - r.finish().unwrap(); - } - - #[test] - fn rejects_truncated_and_oversized() { - let mut r = Reader::new(&[0, 0]); - assert_eq!(r.get_u32(), Err(DecodeError::Truncated)); - - // A length word larger than MAX_FIELD_BYTES is refused before alloc. - let mut w = Writer::new(); - w.put_u32((MAX_FIELD_BYTES + 1) as u32); - let bytes = w.into_vec(); - let mut r = Reader::new(&bytes); - assert_eq!(r.get_bytes(), Err(DecodeError::FieldTooLong)); - } - - #[test] - fn rejects_trailing_bytes() { - let mut w = Writer::new(); - w.put_u8(1); - let mut bytes = w.into_vec(); - bytes.push(0xff); - let mut r = Reader::new(&bytes); - assert_eq!(r.get_u8().unwrap(), 1); - assert_eq!(r.finish(), Err(DecodeError::TrailingBytes)); - } -} diff --git a/packages/d2b-exec-runner/src/filering.rs b/packages/d2b-exec-runner/src/filering.rs deleted file mode 100644 index 5720a74029..0000000000 --- a/packages/d2b-exec-runner/src/filering.rs +++ /dev/null @@ -1,784 +0,0 @@ -//! File-backed, drop-oldest output ring with monotonic logical offsets. -//! -//! The semantics mirror guestd's in-memory `OutputRing` exactly (a parity test -//! asserts this), but the bytes live in a fixed-size circular file on tmpfs so -//! the runner (writer) and guestd (reader) can be different processes and so -//! the bytes survive a guestd restart for re-adoption. -//! -//! Layout per stream: -//! - `` - a circular data file whose physical size never exceeds -//! `cap`. The byte at logical offset `o` lives at physical `o % cap`. -//! - `.meta` - a small [`StreamMeta`] sidecar written atomically -//! (temp -> fsync -> rename -> dir fsync). -//! -//! Durability ordering (the crux of the cross-process protocol): -//! - When an append drops oldest bytes (advancing `start_offset`), the writer -//! persists the advanced `start_offset` BEFORE physically overwriting the -//! cells, so a concurrent reader observes the start advance and expires the -//! offset rather than reading a torn region. -//! - Data is always flushed (`sync_data`) BEFORE the sidecar publishes a larger -//! `end_offset`, so a reader never sees offsets ahead of durable bytes. -//! -//! This module is dependency-pure (std only); symlink-safe opens use -//! `O_NOFOLLOW` via `OpenOptionsExt::custom_flags`. - -use std::fs::{File, OpenOptions}; -use std::io; -use std::os::unix::fs::{FileExt, OpenOptionsExt}; -use std::path::{Path, PathBuf}; - -use crate::atomicio::{O_NOFOLLOW, atomic_write, open_read_nofollow, read_file_nofollow}; -use crate::codec::{DecodeError, Reader, Writer}; - -const META_MAGIC: u32 = 0x4e4c_534d; // "NLSM" -const META_VERSION: u32 = 1; - -/// Bounded retries for the reader's torn-read detection. -const READ_RETRIES: u32 = 8; - -/// The sidecar contents: the durable ring bookkeeping for one stream. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct StreamMeta { - pub cap: u64, - /// Logical offset of the oldest retained byte (== `dropped_bytes`). - pub start_offset: u64, - /// Logical offset one past the newest byte (== total bytes ever written). - pub end_offset: u64, - /// Count of bytes dropped off the front (always equal to `start_offset`). - pub dropped_bytes: u64, - /// True once any byte has been dropped. - pub truncated: bool, - /// True once the stream has been closed by the writer. - pub eof: bool, - /// True when guestd marked the stream lost (runner/unit vanished without a - /// clean EOF). ORs into a chunk's `truncated`. - pub lost: bool, -} - -impl StreamMeta { - fn new(cap: u64) -> Self { - Self { - cap, - start_offset: 0, - end_offset: 0, - dropped_bytes: 0, - truncated: false, - eof: false, - lost: false, - } - } - - fn encode(&self) -> Vec { - let mut w = Writer::new(); - w.put_u32(META_MAGIC); - w.put_u32(META_VERSION); - w.put_u64(self.cap); - w.put_u64(self.start_offset); - w.put_u64(self.end_offset); - w.put_u64(self.dropped_bytes); - w.put_bool(self.truncated); - w.put_bool(self.eof); - w.put_bool(self.lost); - w.into_vec() - } - - fn decode(bytes: &[u8]) -> Result { - let mut r = Reader::new(bytes); - if r.get_u32()? != META_MAGIC { - return Err(DecodeError::BadMagic); - } - if r.get_u32()? != META_VERSION { - return Err(DecodeError::BadVersion); - } - let cap = r.get_u64()?; - let start_offset = r.get_u64()?; - let end_offset = r.get_u64()?; - let dropped_bytes = r.get_u64()?; - let truncated = r.get_bool()?; - let eof = r.get_bool()?; - let lost = r.get_bool()?; - r.finish()?; - Ok(Self { - cap, - start_offset, - end_offset, - dropped_bytes, - truncated, - eof, - lost, - }) - } -} - -/// Result of a ring read. Field-for-field compatible with guestd's `RingChunk`. -#[derive(Clone, PartialEq, Eq)] -pub struct RingChunk { - pub data: Vec, - pub start_offset: u64, - pub end_offset: u64, - pub next_offset: u64, - pub dropped_bytes: u64, - pub truncated: bool, - pub eof: bool, -} - -// Redacted Debug: the captured `data` bytes are NEVER printed (they would leak -// child stdout/stderr into any log/panic/test-failure message). Only the -// bounded length and metadata are surfaced. -impl std::fmt::Debug for RingChunk { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RingChunk") - .field("data_len", &self.data.len()) - .field("start_offset", &self.start_offset) - .field("end_offset", &self.end_offset) - .field("next_offset", &self.next_offset) - .field("dropped_bytes", &self.dropped_bytes) - .field("truncated", &self.truncated) - .field("eof", &self.eof) - .finish() - } -} - -/// Typed FileRing failure. Carries no payload bytes. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileRingError { - Io(io::ErrorKind), - Decode(DecodeError), - OffsetExpired, - OffsetInFuture, - CapMismatch, - /// The sidecar metadata is internally inconsistent or inconsistent with the - /// data file (corrupt/tampered pair); never serve bytes from it. - Corrupt, - /// The reader could not obtain a stable (un-torn) read within the retry - /// budget; the caller should treat this as an expired offset. - Busy, -} - -impl From for FileRingError { - fn from(error: io::Error) -> Self { - FileRingError::Io(error.kind()) - } -} - -impl From for FileRingError { - fn from(error: DecodeError) -> Self { - FileRingError::Decode(error) - } -} - -fn open_nofollow_write(path: &Path) -> io::Result { - OpenOptions::new() - .create(true) - .read(true) - .write(true) - .truncate(false) - // Privileged captured output; create 0600 explicitly, never umask. - .mode(crate::atomicio::FILE_MODE_0600) - .custom_flags(O_NOFOLLOW) - .open(path) -} - -/// Atomically replace the sidecar at `sidecar_path` with `meta`. -fn persist_meta(sidecar_path: &Path, meta: &StreamMeta) -> io::Result<()> { - atomic_write(sidecar_path, &meta.encode()) -} - -fn read_meta(sidecar_path: &Path) -> Result { - let bytes = read_file_nofollow(sidecar_path)?; - StreamMeta::decode(&bytes).map_err(FileRingError::from) -} - -/// Validate a decoded sidecar before any byte is served from the ring. Rejects -/// a corrupt/tampered pair (offsets ahead of data, impossible cap, -/// dropped/start drift) so the reader never indexes an invalid ring region. -fn validate_meta(meta: &StreamMeta, data_len: u64) -> Result<(), FileRingError> { - // cap must be positive and within the largest cap the framework ever - // reserves (a per-stream retained-log cap). Test rings use much smaller - // caps, all well under this ceiling. - if meta.cap == 0 || meta.cap > crate::DETACHED_STREAM_LOG_BYTES { - return Err(FileRingError::Corrupt); - } - // Monotonic offsets: start never exceeds end. - if meta.start_offset > meta.end_offset { - return Err(FileRingError::Corrupt); - } - // The retained span can never exceed the ring capacity. - if meta.end_offset - meta.start_offset > meta.cap { - return Err(FileRingError::Corrupt); - } - // dropped_bytes is, by construction, exactly the advanced start offset. - if meta.dropped_bytes != meta.start_offset { - return Err(FileRingError::Corrupt); - } - // The physical data file must cover every cell we could index: the live - // region is `min(end_offset, cap)` bytes (the file only grows to `cap`). - let required_physical = meta.end_offset.min(meta.cap); - if data_len < required_physical { - return Err(FileRingError::Corrupt); - } - Ok(()) -} - -/// The writer side of a stream's ring (owned by the runner). -pub struct FileRing { - data: File, - data_path: PathBuf, - sidecar_path: PathBuf, - meta: StreamMeta, -} - -impl FileRing { - /// Create (or reopen) a stream's data + sidecar files with capacity `cap`. - /// A fresh stream starts empty; reopening an existing stream restores its - /// persisted meta (used by the runner only for its own freshly created - /// slot, never to adopt a foreign one). - pub fn create(data_path: &Path, sidecar_path: &Path, cap: u64) -> Result { - assert!(cap > 0, "ring capacity must be positive"); - let data = open_nofollow_write(data_path)?; - let meta = StreamMeta::new(cap); - persist_meta(sidecar_path, &meta)?; - Ok(Self { - data, - data_path: data_path.to_path_buf(), - sidecar_path: sidecar_path.to_path_buf(), - meta, - }) - } - - pub fn meta(&self) -> StreamMeta { - self.meta - } - - pub fn data_path(&self) -> &Path { - &self.data_path - } - - pub fn sidecar_path(&self) -> &Path { - &self.sidecar_path - } - - /// Append `bytes`, dropping oldest bytes beyond `cap`. Persists the sidecar - /// durably (start-before-overwrite, data-before-end). - pub fn append(&mut self, bytes: &[u8]) -> Result<(), FileRingError> { - if bytes.is_empty() { - return Ok(()); - } - let cap = self.meta.cap; - let incoming = bytes.len() as u64; - let new_end = self.meta.end_offset.saturating_add(incoming); - let new_start = new_end.saturating_sub(cap); - - // Phase 1: if this append drops oldest bytes, publish the advanced - // start (without advancing end) BEFORE physically overwriting cells, so - // a concurrent reader expires the offset instead of reading a torn - // region. `end` is left unchanged here. - if new_start > self.meta.start_offset { - let pre = StreamMeta { - start_offset: new_start.min(self.meta.end_offset), - dropped_bytes: new_start.min(self.meta.end_offset), - truncated: true, - ..self.meta - }; - persist_meta(&self.sidecar_path, &pre)?; - self.meta = pre; - } - - // Only the surviving tail of the incoming bytes is worth writing. - let write_from = new_start.max(self.meta.end_offset); - let skip = (write_from - self.meta.end_offset) as usize; - let payload = &bytes[skip..]; - self.write_circular(write_from, payload)?; - - // Phase 3: flush data before publishing the larger end. - self.data.sync_data()?; - - // Phase 4: publish the final {start, end} snapshot. - self.meta.end_offset = new_end; - if new_start > self.meta.start_offset { - self.meta.start_offset = new_start; - self.meta.dropped_bytes = new_start; - self.meta.truncated = true; - } - persist_meta(&self.sidecar_path, &self.meta)?; - Ok(()) - } - - /// Mark the stream closed (clean EOF). - pub fn mark_eof(&mut self) -> Result<(), FileRingError> { - if self.meta.eof { - return Ok(()); - } - self.meta.eof = true; - persist_meta(&self.sidecar_path, &self.meta)?; - Ok(()) - } - - /// Mark the stream lost (writer/unit vanished). Sets `lost` and `eof` so - /// readers terminate; `lost` ORs into chunk `truncated`. - pub fn mark_lost(&mut self) -> Result<(), FileRingError> { - if self.meta.lost && self.meta.eof { - return Ok(()); - } - self.meta.lost = true; - self.meta.eof = true; - persist_meta(&self.sidecar_path, &self.meta)?; - Ok(()) - } - - fn write_circular(&self, logical_start: u64, data: &[u8]) -> Result<(), FileRingError> { - if data.is_empty() { - return Ok(()); - } - let cap = self.meta.cap as usize; - let mut pos = (logical_start % self.meta.cap) as usize; - let mut rem = data; - while !rem.is_empty() { - let space = cap - pos; - let n = rem.len().min(space); - self.data.write_all_at(&rem[..n], pos as u64)?; - pos = (pos + n) % cap; - rem = &rem[n..]; - } - Ok(()) - } -} - -/// The reader side of a stream's ring (owned by guestd). -pub struct FileRingReader { - data: File, - sidecar_path: PathBuf, -} - -impl FileRingReader { - pub fn open(data_path: &Path, sidecar_path: &Path) -> Result { - let data = open_read_nofollow(data_path)?; - Ok(Self { - data, - sidecar_path: sidecar_path.to_path_buf(), - }) - } - - /// Read the sidecar and validate it is internally consistent and consistent - /// with the actual data-file length before any byte is served. A corrupt or - /// tampered sidecar (offsets ahead of data, impossible cap, dropped/start - /// drift) is rejected as [`FileRingError::Corrupt`] rather than used to read - /// from an invalid ring region. - fn read_validated_meta(&self) -> Result { - let meta = read_meta(&self.sidecar_path)?; - let data_len = self - .data - .metadata() - .map(|m| m.len()) - .map_err(FileRingError::from)?; - validate_meta(&meta, data_len)?; - Ok(meta) - } - - pub fn meta(&self) -> Result { - self.read_validated_meta() - } - - /// Read up to `max_len` bytes starting at logical `offset`. Mirrors - /// `OutputRing::read`: `OffsetExpired` if `offset < start_offset`, - /// `OffsetInFuture` if `offset > end_offset`. - pub fn read(&self, offset: u64, max_len: u64) -> Result { - let mut last_err = FileRingError::Busy; - for _ in 0..READ_RETRIES { - let meta = self.read_validated_meta()?; - if offset < meta.start_offset { - return Err(FileRingError::OffsetExpired); - } - if offset > meta.end_offset { - return Err(FileRingError::OffsetInFuture); - } - let available = meta.end_offset - offset; - let take = available.min(max_len); - let data = match self.read_circular(offset, take, meta.cap) { - Ok(data) => data, - Err(err) => { - last_err = err; - continue; - } - }; - - // Torn-read detection: if the writer advanced start past our offset - // during the read, the physical region may have been overwritten. - let after = self.read_validated_meta()?; - if after.start_offset > offset { - last_err = FileRingError::OffsetExpired; - continue; - } - - let next_offset = offset.saturating_add(take); - return Ok(RingChunk { - data, - start_offset: meta.start_offset, - end_offset: meta.end_offset, - next_offset, - dropped_bytes: meta.dropped_bytes, - truncated: meta.truncated || meta.lost, - eof: meta.eof && next_offset >= meta.end_offset, - }); - } - Err(last_err) - } - - fn read_circular(&self, offset: u64, take: u64, cap: u64) -> Result, FileRingError> { - if take == 0 { - return Ok(Vec::new()); - } - if cap == 0 { - return Err(FileRingError::CapMismatch); - } - let cap_usize = cap as usize; - let mut out = vec![0u8; take as usize]; - let mut pos = (offset % cap) as usize; - let mut written = 0usize; - while written < out.len() { - let space = cap_usize - pos; - let n = (out.len() - written).min(space); - self.data - .read_exact_at(&mut out[written..written + n], pos as u64)?; - pos = (pos + n) % cap_usize; - written += n; - } - Ok(out) - } -} - -/// Mark a stream lost given only its sidecar path (guestd live reconciliation -/// when no [`FileRing`] writer is held). Idempotent. -pub fn mark_stream_lost(sidecar_path: &Path) -> Result<(), FileRingError> { - let mut meta = read_meta(sidecar_path)?; - if meta.lost && meta.eof { - return Ok(()); - } - meta.lost = true; - meta.eof = true; - persist_meta(sidecar_path, &meta)?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Local copy of guestd's OutputRing semantics for the parity assertion. - struct ModelRing { - start_offset: u64, - data: std::collections::VecDeque, - cap: usize, - dropped_bytes: u64, - truncated: bool, - eof: bool, - } - - impl ModelRing { - fn new(cap: usize) -> Self { - Self { - start_offset: 0, - data: std::collections::VecDeque::new(), - cap, - dropped_bytes: 0, - truncated: false, - eof: false, - } - } - - fn end_offset(&self) -> u64 { - self.start_offset.saturating_add(self.data.len() as u64) - } - - fn append(&mut self, bytes: &[u8]) { - self.data.extend(bytes.iter().copied()); - while self.data.len() > self.cap { - if self.data.pop_front().is_some() { - self.start_offset = self.start_offset.saturating_add(1); - self.dropped_bytes = self.dropped_bytes.saturating_add(1); - self.truncated = true; - } else { - break; - } - } - } - - fn read(&self, offset: u64, max_len: u64) -> Result { - let end = self.end_offset(); - if offset < self.start_offset { - return Err(FileRingError::OffsetExpired); - } - if offset > end { - return Err(FileRingError::OffsetInFuture); - } - let available = end - offset; - let take = available.min(max_len); - let begin = (offset - self.start_offset) as usize; - let data: Vec = self - .data - .iter() - .skip(begin) - .take(take as usize) - .copied() - .collect(); - let next_offset = offset.saturating_add(take); - Ok(RingChunk { - data, - start_offset: self.start_offset, - end_offset: end, - next_offset, - dropped_bytes: self.dropped_bytes, - truncated: self.truncated, - eof: self.eof && next_offset >= end, - }) - } - } - - fn scratch_dir() -> PathBuf { - let base = std::env::var_os("TMPDIR") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(".")); - let unique = format!( - "filering-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - ); - let dir = base.join(unique); - std::fs::create_dir_all(&dir).unwrap(); - dir - } - - fn paths(dir: &Path) -> (PathBuf, PathBuf) { - (dir.join("stdout"), dir.join("stdout.meta")) - } - - #[test] - fn round_trips_without_drop() { - let dir = scratch_dir(); - let (data, side) = paths(&dir); - let mut ring = FileRing::create(&data, &side, 64).unwrap(); - ring.append(b"hello world").unwrap(); - let reader = FileRingReader::open(&data, &side).unwrap(); - let chunk = reader.read(0, 1024).unwrap(); - assert_eq!(chunk.data, b"hello world"); - assert_eq!(chunk.start_offset, 0); - assert_eq!(chunk.end_offset, 11); - assert_eq!(chunk.next_offset, 11); - assert!(!chunk.truncated); - assert!(!chunk.eof); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn eof_only_observable_when_drained() { - let dir = scratch_dir(); - let (data, side) = paths(&dir); - let mut ring = FileRing::create(&data, &side, 64).unwrap(); - ring.append(b"abc").unwrap(); - ring.mark_eof().unwrap(); - let reader = FileRingReader::open(&data, &side).unwrap(); - let partial = reader.read(0, 2).unwrap(); - assert!(!partial.eof, "eof must not show until fully drained"); - let full = reader.read(0, 64).unwrap(); - assert!(full.eof); - assert_eq!(full.data, b"abc"); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn offset_bounds_match_model() { - let dir = scratch_dir(); - let (data, side) = paths(&dir); - let mut ring = FileRing::create(&data, &side, 8).unwrap(); - ring.append(b"0123456789").unwrap(); // 10 bytes into cap 8 -> drop 2 - let reader = FileRingReader::open(&data, &side).unwrap(); - assert_eq!(reader.read(0, 4), Err(FileRingError::OffsetExpired)); - assert_eq!(reader.read(11, 4), Err(FileRingError::OffsetInFuture)); - let chunk = reader.read(2, 100).unwrap(); - assert_eq!(chunk.data, b"23456789"); - assert_eq!(chunk.start_offset, 2); - assert_eq!(chunk.dropped_bytes, 2); - assert!(chunk.truncated); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn lost_flag_ors_into_truncated() { - let dir = scratch_dir(); - let (data, side) = paths(&dir); - let mut ring = FileRing::create(&data, &side, 64).unwrap(); - ring.append(b"abc").unwrap(); - ring.mark_lost().unwrap(); - let reader = FileRingReader::open(&data, &side).unwrap(); - let chunk = reader.read(0, 64).unwrap(); - assert!(chunk.truncated, "lost must OR into truncated"); - assert!(chunk.eof); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn parity_with_model_under_random_appends() { - let dir = scratch_dir(); - let (data, side) = paths(&dir); - let cap = 17u64; - let mut ring = FileRing::create(&data, &side, cap).unwrap(); - let mut model = ModelRing::new(cap as usize); - let reader = FileRingReader::open(&data, &side).unwrap(); - - // Deterministic pseudo-random byte stream + append sizes. - let mut state = 0x12345678u32; - let mut next = || { - state = state.wrapping_mul(1103515245).wrapping_add(12345); - (state >> 16) as u8 - }; - - for round in 0..200u32 { - let len = (next() % 13) as usize; - let bytes: Vec = (0..len).map(|_| next()).collect(); - ring.append(&bytes).unwrap(); - model.append(&bytes); - - // Probe several offsets/lengths and compare chunks. - for probe in 0..4u32 { - let end = model.end_offset(); - let off = if end == 0 { - 0 - } else { - (next() as u64 + probe as u64) % (end + 1) - }; - let max_len = (next() as u64 % (cap + 4)).max(1); - let got = reader.read(off, max_len); - let want = model.read(off, max_len); - assert_eq!( - got, want, - "parity mismatch at round {round} off {off} max {max_len}" - ); - } - } - - ring.mark_eof().unwrap(); - model.eof = true; - let end = model.end_offset(); - assert_eq!(reader.read(end, 8).unwrap(), model.read(end, 8).unwrap()); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn meta_round_trips() { - let meta = StreamMeta { - cap: 4096, - start_offset: 10, - end_offset: 4106, - dropped_bytes: 10, - truncated: true, - eof: false, - lost: false, - }; - let decoded = StreamMeta::decode(&meta.encode()).unwrap(); - assert_eq!(meta, decoded); - } - - #[test] - fn reader_rejects_sidecar_end_ahead_of_data() { - let dir = scratch_dir(); - let (data, side) = paths(&dir); - let mut ring = FileRing::create(&data, &side, 64).unwrap(); - ring.append(b"abc").unwrap(); - // Tamper: claim more bytes than the data file actually holds. - let corrupt = StreamMeta { - cap: 64, - start_offset: 0, - end_offset: 1000, - dropped_bytes: 0, - truncated: false, - eof: false, - lost: false, - }; - persist_meta(&side, &corrupt).unwrap(); - let reader = FileRingReader::open(&data, &side).unwrap(); - assert_eq!(reader.meta(), Err(FileRingError::Corrupt)); - assert_eq!(reader.read(0, 64), Err(FileRingError::Corrupt)); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn reader_rejects_inconsistent_dropped_and_impossible_cap() { - let dir = scratch_dir(); - let (data, side) = paths(&dir); - let mut ring = FileRing::create(&data, &side, 64).unwrap(); - ring.append(b"hello").unwrap(); - let reader = FileRingReader::open(&data, &side).unwrap(); - - // dropped_bytes drifts from start_offset. - persist_meta( - &side, - &StreamMeta { - cap: 64, - start_offset: 0, - end_offset: 5, - dropped_bytes: 3, - truncated: false, - eof: false, - lost: false, - }, - ) - .unwrap(); - assert_eq!(reader.read(0, 64), Err(FileRingError::Corrupt)); - - // start > end. - persist_meta( - &side, - &StreamMeta { - cap: 64, - start_offset: 9, - end_offset: 5, - dropped_bytes: 9, - truncated: true, - eof: false, - lost: false, - }, - ) - .unwrap(); - assert_eq!(reader.read(0, 64), Err(FileRingError::Corrupt)); - - // Impossible cap (above the per-stream retained ceiling). - persist_meta( - &side, - &StreamMeta { - cap: crate::DETACHED_STREAM_LOG_BYTES + 1, - start_offset: 0, - end_offset: 5, - dropped_bytes: 0, - truncated: false, - eof: false, - lost: false, - }, - ) - .unwrap(); - assert_eq!(reader.read(0, 64), Err(FileRingError::Corrupt)); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn ring_chunk_debug_redacts_payload_bytes() { - let chunk = RingChunk { - data: b"super secret stdout".to_vec(), - start_offset: 3, - end_offset: 22, - next_offset: 22, - dropped_bytes: 3, - truncated: true, - eof: true, - }; - let rendered = format!("{chunk:?}"); - assert!( - !rendered.contains("secret"), - "payload must never appear: {rendered}" - ); - assert!(rendered.contains("data_len")); - assert!(rendered.contains("19")); - } -} diff --git a/packages/d2b-exec-runner/src/lib.rs b/packages/d2b-exec-runner/src/lib.rs deleted file mode 100644 index 07d9b1d4de..0000000000 --- a/packages/d2b-exec-runner/src/lib.rs +++ /dev/null @@ -1,288 +0,0 @@ -#![doc = "Guest-side detached exec runner: validation primitives, the on-disk"] -#![doc = "spec/record/status codecs, the file-backed output ring, and the"] -#![doc = "slot-keyed path layout. This library is dependency-pure (std only) so"] -#![doc = "the guest-static dependency-policy gate stays green; the binary may use"] -#![doc = "rustix for the privileged service-mode syscalls."] - -pub mod atomicio; -pub mod codec; -pub mod filering; -pub mod limits; -pub mod paths; -pub mod record; -pub mod spec; - -pub use filering::{FileRing, FileRingError, RingChunk, StreamMeta}; -pub use limits::{ - DETACHED_ACTIVE_PER_VM, DETACHED_LOG_QUOTA_BYTES, DETACHED_PER_EXEC_LOG_BYTES, - DETACHED_RETAINED_PER_VM, DETACHED_STREAM_LOG_BYTES, -}; -pub use paths::{RunnerPaths, Stream}; -pub use record::{DurableRecord, RecordState, StatusPhase, StatusRecord}; -pub use spec::{ExecSpec, SpecCodec, SpecError}; - -pub const MAX_ARGV: usize = 128; -pub const MAX_ARG_LEN: usize = 4096; -pub const MAX_CWD_LEN: usize = 4096; -pub const MAX_ENV: usize = 256; -pub const MAX_ENV_KEY_LEN: usize = 128; -pub const MAX_ENV_VALUE_LEN: usize = 8192; - -#[derive(Clone, PartialEq, Eq)] -pub struct RunnerCommand { - pub argv: Vec, - pub cwd: Option, - pub env: Vec, -} - -#[derive(Clone, PartialEq, Eq)] -pub struct RunnerEnv { - pub key: String, - pub value: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValidationError { - EmptyArgv, - TooManyArgs, - ArgEmpty, - ArgLeadingDash, - ArgTooLong, - ArgContainsNul, - CwdEmpty, - CwdRelative, - CwdTooLong, - CwdContainsNul, - TooManyEnv, - EnvKeyEmpty, - EnvKeyTooLong, - EnvKeyContainsNul, - EnvKeyContainsEquals, - EnvValueTooLong, - EnvValueContainsNul, -} - -impl RunnerCommand { - pub fn validate(&self) -> Result<(), ValidationError> { - validate_argv(&self.argv)?; - validate_cwd(self.cwd.as_deref())?; - validate_env(&self.env) - } -} - -pub fn validate_argv(argv: &[String]) -> Result<(), ValidationError> { - if argv.is_empty() { - return Err(ValidationError::EmptyArgv); - } - if argv.len() > MAX_ARGV { - return Err(ValidationError::TooManyArgs); - } - for arg in argv { - if arg.is_empty() { - return Err(ValidationError::ArgEmpty); - } - if arg.len() > MAX_ARG_LEN { - return Err(ValidationError::ArgTooLong); - } - if contains_nul(arg) { - return Err(ValidationError::ArgContainsNul); - } - } - if argv[0].starts_with('-') { - return Err(ValidationError::ArgLeadingDash); - } - Ok(()) -} - -pub fn validate_cwd(cwd: Option<&str>) -> Result<(), ValidationError> { - let Some(cwd) = cwd else { - return Ok(()); - }; - if cwd.is_empty() { - return Err(ValidationError::CwdEmpty); - } - if contains_nul(cwd) { - return Err(ValidationError::CwdContainsNul); - } - if !cwd.starts_with('/') { - return Err(ValidationError::CwdRelative); - } - if cwd.len() > MAX_CWD_LEN { - return Err(ValidationError::CwdTooLong); - } - Ok(()) -} - -pub fn validate_env(env: &[RunnerEnv]) -> Result<(), ValidationError> { - if env.len() > MAX_ENV { - return Err(ValidationError::TooManyEnv); - } - for entry in env { - if entry.key.is_empty() { - return Err(ValidationError::EnvKeyEmpty); - } - if entry.key.len() > MAX_ENV_KEY_LEN { - return Err(ValidationError::EnvKeyTooLong); - } - if contains_nul(&entry.key) { - return Err(ValidationError::EnvKeyContainsNul); - } - if entry.key.contains('=') { - return Err(ValidationError::EnvKeyContainsEquals); - } - if entry.value.len() > MAX_ENV_VALUE_LEN { - return Err(ValidationError::EnvValueTooLong); - } - if contains_nul(&entry.value) { - return Err(ValidationError::EnvValueContainsNul); - } - } - Ok(()) -} - -fn contains_nul(value: &str) -> bool { - value.as_bytes().contains(&0) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn valid_command() -> RunnerCommand { - RunnerCommand { - argv: vec!["/bin/true".to_owned()], - cwd: Some("/home/alice".to_owned()), - env: vec![RunnerEnv { - key: "PATH".to_owned(), - value: "/run/current-system/sw/bin".to_owned(), - }], - } - } - - #[test] - fn accepts_bounded_command() { - assert!(valid_command().validate().is_ok()); - assert!(validate_argv(&["x".repeat(MAX_ARG_LEN)]).is_ok()); - assert!(validate_argv(&vec!["x".to_owned(); MAX_ARGV]).is_ok()); - assert!(validate_cwd(Some(&format!("/{}", "x".repeat(MAX_CWD_LEN - 1)))).is_ok()); - assert!( - validate_env(&[RunnerEnv { - key: "K".repeat(MAX_ENV_KEY_LEN), - value: "V".repeat(MAX_ENV_VALUE_LEN), - }]) - .is_ok() - ); - assert!( - validate_env(&vec![ - RunnerEnv { - key: "K".to_owned(), - value: "V".to_owned(), - }; - MAX_ENV - ]) - .is_ok() - ); - } - - #[test] - fn rejects_empty_or_too_many_args() { - assert_eq!( - RunnerCommand { - argv: Vec::new(), - cwd: None, - env: Vec::new(), - } - .validate(), - Err(ValidationError::EmptyArgv) - ); - assert_eq!( - validate_argv(&vec!["x".to_owned(); MAX_ARGV + 1]), - Err(ValidationError::TooManyArgs) - ); - } - - #[test] - fn rejects_unbounded_or_nul_values_without_echoing_payloads() { - assert_eq!( - validate_argv(&["x".repeat(MAX_ARG_LEN + 1)]), - Err(ValidationError::ArgTooLong) - ); - assert_eq!( - validate_argv(&["".to_owned()]), - Err(ValidationError::ArgEmpty) - ); - assert_eq!( - validate_argv(&["-bad".to_owned()]), - Err(ValidationError::ArgLeadingDash) - ); - assert_eq!( - validate_argv(&["bad\0arg".to_owned()]), - Err(ValidationError::ArgContainsNul) - ); - assert_eq!( - validate_cwd(Some("a\0b")), - Err(ValidationError::CwdContainsNul) - ); - assert_eq!(validate_cwd(Some("")), Err(ValidationError::CwdEmpty)); - assert_eq!( - validate_cwd(Some("relative")), - Err(ValidationError::CwdRelative) - ); - assert_eq!( - validate_cwd(Some(&format!("/{}", "x".repeat(MAX_CWD_LEN)))), - Err(ValidationError::CwdTooLong) - ); - assert_eq!( - validate_env(&vec![ - RunnerEnv { - key: "K".to_owned(), - value: "V".to_owned(), - }; - MAX_ENV + 1 - ]), - Err(ValidationError::TooManyEnv) - ); - assert_eq!( - validate_env(&[RunnerEnv { - key: "".to_owned(), - value: "value".to_owned(), - }]), - Err(ValidationError::EnvKeyEmpty) - ); - assert_eq!( - validate_env(&[RunnerEnv { - key: "K".repeat(MAX_ENV_KEY_LEN + 1), - value: "value".to_owned(), - }]), - Err(ValidationError::EnvKeyTooLong) - ); - assert_eq!( - validate_env(&[RunnerEnv { - key: "BAD\0KEY".to_owned(), - value: "value".to_owned(), - }]), - Err(ValidationError::EnvKeyContainsNul) - ); - assert_eq!( - validate_env(&[RunnerEnv { - key: "BAD=KEY".to_owned(), - value: "value".to_owned(), - }]), - Err(ValidationError::EnvKeyContainsEquals) - ); - assert_eq!( - validate_env(&[RunnerEnv { - key: "TOKEN".to_owned(), - value: "x".repeat(MAX_ENV_VALUE_LEN + 1), - }]), - Err(ValidationError::EnvValueTooLong) - ); - assert_eq!( - validate_env(&[RunnerEnv { - key: "TOKEN".to_owned(), - value: "bad\0value".to_owned(), - }]), - Err(ValidationError::EnvValueContainsNul) - ); - } -} diff --git a/packages/d2b-exec-runner/src/limits.rs b/packages/d2b-exec-runner/src/limits.rs deleted file mode 100644 index 84a5c48b7a..0000000000 --- a/packages/d2b-exec-runner/src/limits.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Detached-exec capacity, quota, and retention constants shared by the -//! runner (writer) and guestd (reader/supervisor) so both agree on the exact -//! worst-case reservation. The quota invariant -//! (`quota == slots * 2 * stream_cap`) is asserted by a unit test below. - -/// Maximum concurrent *running* detached execs (a concurrency cap, NOT a -/// runtime cap - each may run indefinitely). -pub const DETACHED_ACTIVE_PER_VM: usize = 8; - -/// Total retained detached records (running + terminal) per VM. Equals the -/// number of slots. -pub const DETACHED_RETAINED_PER_VM: usize = 32; - -/// Per-stream retained-log byte cap (drop-oldest). Bounds the tmpfs worst -/// case; advertised via `effective_limits`. -pub const DETACHED_STREAM_LOG_BYTES: u64 = 4 * 1024 * 1024; - -/// VM-global retained-log byte quota. Defined to be exactly -/// `slots * 2 streams * stream_cap` so the worst-case reservation is exact -/// and an adopted-over-budget state is structurally impossible. -pub const DETACHED_LOG_QUOTA_BYTES: u64 = - (DETACHED_RETAINED_PER_VM as u64) * 2 * DETACHED_STREAM_LOG_BYTES; - -/// Per-exec reserved retained-log bytes (both streams at the cap). -pub const DETACHED_PER_EXEC_LOG_BYTES: u64 = 2 * DETACHED_STREAM_LOG_BYTES; - -/// Retention TTL after a record reaches a terminal state (TERMINAL records -/// only; a Running detached job is never reaped by TTL). -pub const DETACHED_RETENTION_TTL_MS: u64 = 30 * 60 * 1_000; - -/// Bounded wait for the runner's first phase marker before a create call -/// resolves via a unit re-query. -pub const CREATE_TIMEOUT_MS: u64 = 10_000; - -/// Bounded persisted dispatch deadline: a crash-recovered no-unit -/// `Dispatching` record is held in-flight (slot reserved, non-listable) until -/// this elapses with a negative unit re-query. -pub const DISPATCH_DEADLINE_MS: u64 = 30_000; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn quota_invariant_holds_exactly() { - assert_eq!( - DETACHED_LOG_QUOTA_BYTES, - (DETACHED_RETAINED_PER_VM as u64) * 2 * DETACHED_STREAM_LOG_BYTES, - "quota must equal slots * 2 streams * stream cap exactly" - ); - assert_eq!(DETACHED_LOG_QUOTA_BYTES, 256 * 1024 * 1024); - assert_eq!(DETACHED_PER_EXEC_LOG_BYTES, 2 * DETACHED_STREAM_LOG_BYTES); - // Per-exec reservation times slot count is exactly the global quota. - assert_eq!( - DETACHED_PER_EXEC_LOG_BYTES * (DETACHED_RETAINED_PER_VM as u64), - DETACHED_LOG_QUOTA_BYTES - ); - } -} diff --git a/packages/d2b-exec-runner/src/main.rs b/packages/d2b-exec-runner/src/main.rs deleted file mode 100644 index 1f44d7e752..0000000000 --- a/packages/d2b-exec-runner/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! `d2b-exec-runner` binary entry point. -//! -//! Three invocations are supported: -//! - `--version` prints the crate version and exits 0. -//! - `--serve-exec --slot NN` runs the per-slot detached supervisor (see -//! [`service_mode`]). -//! - `--tty-exec --rows R --cols C -- ` is the interactive TTY exec -//! helper: it makes itself the session leader with the inherited PTY slave as -//! its controlling terminal, then `exec`s the target (see [`tty_helper`]). -//! -//! Any other invocation fails closed with exit 78 so a misconfigured unit can -//! never silently degrade into an unsupervised exec. - -use std::{env, process}; - -mod service_mode; -mod tty_helper; - -fn main() { - let args: Vec = env::args().skip(1).collect(); - process::exit(run(&args)); -} - -fn run(args: &[String]) -> i32 { - match args.first().map(String::as_str) { - Some("--version") if args.len() == 1 => { - println!("d2b-exec-runner {}", env!("CARGO_PKG_VERSION")); - 0 - } - Some("--serve-exec") => match parse_slot(&args[1..]) { - Some(slot) => service_mode::main_service(slot), - None => { - eprintln!("d2b-exec-runner: --serve-exec requires --slot <0..32>"); - 64 - } - }, - // Interactive TTY exec helper. guestd spawns this with the PTY - // slave on stdin and a CLOEXEC status pipe on stdout; the helper makes - // itself the session leader with the slave as its controlling terminal, - // then exec's the target. See `tty_helper`. - Some("--tty-exec") => tty_helper::run(&args[1..]), - _ => { - eprintln!("d2b-exec-runner: unsupported invocation"); - 78 - } - } -} - -/// Parse exactly `--slot ` from the remaining args. -fn parse_slot(rest: &[String]) -> Option { - match rest { - [flag, value] if flag == "--slot" => value.parse::().ok(), - _ => None, - } -} diff --git a/packages/d2b-exec-runner/src/paths.rs b/packages/d2b-exec-runner/src/paths.rs deleted file mode 100644 index 64bd60700e..0000000000 --- a/packages/d2b-exec-runner/src/paths.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Slot-keyed on-disk layout for a detached exec. -//! -//! Every live/retained detached exec owns a bounded slot index in -//! `[0, DETACHED_RETAINED_PER_VM)`. The slot directory and its files are -//! derived purely from the slot, never from the opaque exec id, so guest -//! journald `_SYSTEMD_UNIT`/`ExecStart` cardinality stays bounded to <= 32 -//! stable values that carry no exec id. - -use std::path::{Path, PathBuf}; - -/// Canonical parent for all detached-exec slot directories. Root-owned 0700, -/// boot-scoped (see the nixos `systemd.tmpfiles` rule). -pub const RUN_DIR: &str = "/run/d2b-exec"; - -/// One captured output stream. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Stream { - Stdout, - Stderr, -} - -impl Stream { - fn data_name(self) -> &'static str { - match self { - Stream::Stdout => "stdout", - Stream::Stderr => "stderr", - } - } - - fn meta_name(self) -> &'static str { - match self { - Stream::Stdout => "stdout.meta", - Stream::Stderr => "stderr.meta", - } - } -} - -/// Resolves the file paths for a single slot directory under a configurable -/// base (the production base is [`RUN_DIR`]; tests inject a temp dir). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RunnerPaths { - base: PathBuf, - slot: u32, -} - -impl RunnerPaths { - pub fn new(base: impl Into, slot: u32) -> Self { - Self { - base: base.into(), - slot, - } - } - - /// Production layout rooted at [`RUN_DIR`]. - pub fn for_slot(slot: u32) -> Self { - Self::new(RUN_DIR, slot) - } - - pub fn base(&self) -> &Path { - &self.base - } - - pub fn slot(&self) -> u32 { - self.slot - } - - /// `slot-` directory name (zero-padded width 2 for the <= 32 range). - pub fn slot_dir_name(&self) -> String { - format!("slot-{:02}", self.slot) - } - - pub fn slot_dir(&self) -> PathBuf { - self.base.join(self.slot_dir_name()) - } - - pub fn record(&self) -> PathBuf { - self.slot_dir().join("record") - } - - pub fn spec(&self) -> PathBuf { - self.slot_dir().join("spec") - } - - pub fn status(&self) -> PathBuf { - self.slot_dir().join("status") - } - - pub fn cancel(&self) -> PathBuf { - self.slot_dir().join("cancel") - } - - pub fn data(&self, stream: Stream) -> PathBuf { - self.slot_dir().join(stream.data_name()) - } - - pub fn sidecar(&self, stream: Stream) -> PathBuf { - self.slot_dir().join(stream.meta_name()) - } - - /// Static basenames of every per-slot file the framework may create. Used - /// by the re-adoption authenticity gate (per-file `openat`/`O_NOFOLLOW` - /// `fstat`) and by stale-file scrubbing on slot reuse. - pub fn slot_file_names() -> [&'static str; 8] { - [ - "record", - "spec", - "status", - "cancel", - "stdout", - "stderr", - "stdout.meta", - "stderr.meta", - ] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn slot_dir_is_zero_padded_and_id_free() { - let paths = RunnerPaths::new("/run/d2b-exec", 7); - assert_eq!(paths.slot_dir_name(), "slot-07"); - assert_eq!( - paths.slot_dir(), - Path::new("/run/d2b-exec/slot-07").to_path_buf() - ); - assert!(paths.record().ends_with("slot-07/record")); - assert!(paths.spec().ends_with("slot-07/spec")); - assert!(paths.status().ends_with("slot-07/status")); - assert!(paths.cancel().ends_with("slot-07/cancel")); - assert!(paths.data(Stream::Stdout).ends_with("slot-07/stdout")); - assert!( - paths - .sidecar(Stream::Stderr) - .ends_with("slot-07/stderr.meta") - ); - } -} diff --git a/packages/d2b-exec-runner/src/record.rs b/packages/d2b-exec-runner/src/record.rs deleted file mode 100644 index 5efad2de13..0000000000 --- a/packages/d2b-exec-runner/src/record.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! Durable record + runner status phase markers (binary, no serde). -//! -//! - The `record` file is written and read by guestd (the detached registry's -//! canonical crash-recovery state). The runner never touches it. -//! - The `status` file is written by the runner (the supervisor) and read by -//! guestd to learn the exec's lifecycle phase. - -use crate::codec::{DecodeError, Reader, Writer}; - -const RECORD_MAGIC: u32 = 0x4e4c_4552; // "NLER" -const RECORD_VERSION: u32 = 1; -/// Marks a record as a detached exec record so a lookup can never cross -/// authorization domains (an attached id can never resolve one of these). -const RECORD_KIND_DETACHED: u8 = 1; - -const STATUS_MAGIC: u32 = 0x4e4c_5354; // "NLST" -const STATUS_VERSION: u32 = 1; - -/// Linear durable record state machine. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RecordState { - /// Record + spec written and fsync'd BEFORE `systemd-run`; the durable - /// "intent to start a unit for this slot". - Dispatching, - /// Runner published a `started` status. - Running, - /// Child exited with a code. - Exited, - /// Child was terminated by a signal. - Signaled, - /// Cancelled (explicit cancel, ceiling, or vanished-unit `lost`). - Cancelled, - /// The runner could not spawn the child (legitimate terminal exec). - SpawnFailed, -} - -impl RecordState { - pub fn is_terminal(self) -> bool { - matches!( - self, - RecordState::Exited - | RecordState::Signaled - | RecordState::Cancelled - | RecordState::SpawnFailed - ) - } - - fn tag(self) -> u8 { - match self { - RecordState::Dispatching => 0, - RecordState::Running => 1, - RecordState::Exited => 2, - RecordState::Signaled => 3, - RecordState::Cancelled => 4, - RecordState::SpawnFailed => 5, - } - } - - fn from_tag(tag: u8) -> Result { - Ok(match tag { - 0 => RecordState::Dispatching, - 1 => RecordState::Running, - 2 => RecordState::Exited, - 3 => RecordState::Signaled, - 4 => RecordState::Cancelled, - 5 => RecordState::SpawnFailed, - _ => return Err(DecodeError::InvalidTag), - }) - } -} - -/// The canonical guestd-owned detached record. -#[derive(Clone, PartialEq, Eq)] -pub struct DurableRecord { - pub exec_id: String, - pub slot: u32, - pub boot_id: String, - pub create_time_unix: u64, - pub dispatch_deadline_unix: u64, - pub argv_sha256: String, - pub state: RecordState, - pub exit_code: Option, - pub term_signal: Option, - /// True when the record became `Cancelled` because its unit/runner - /// vanished without a terminal status (live reconciliation). - pub lost: bool, - /// Wall-clock time the record first reached a terminal state (for TTL). - pub terminal_time_unix: Option, -} - -// Redacted Debug: never echo the exec id or argv hash provenance beyond shape. -impl std::fmt::Debug for DurableRecord { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DurableRecord") - .field("slot", &self.slot) - .field("state", &self.state) - .field("lost", &self.lost) - .field("terminal_time_unix", &self.terminal_time_unix) - .finish() - } -} - -impl DurableRecord { - pub fn encode(&self) -> Vec { - let mut w = Writer::new(); - w.put_u32(RECORD_MAGIC); - w.put_u32(RECORD_VERSION); - w.put_u8(RECORD_KIND_DETACHED); - w.put_str(&self.exec_id); - w.put_u32(self.slot); - w.put_str(&self.boot_id); - w.put_u64(self.create_time_unix); - w.put_u64(self.dispatch_deadline_unix); - w.put_str(&self.argv_sha256); - w.put_u8(self.state.tag()); - put_opt_i32(&mut w, self.exit_code); - put_opt_u32(&mut w, self.term_signal); - w.put_bool(self.lost); - put_opt_u64(&mut w, self.terminal_time_unix); - w.into_vec() - } - - pub fn decode(bytes: &[u8]) -> Result { - let mut r = Reader::new(bytes); - if r.get_u32()? != RECORD_MAGIC { - return Err(DecodeError::BadMagic); - } - if r.get_u32()? != RECORD_VERSION { - return Err(DecodeError::BadVersion); - } - if r.get_u8()? != RECORD_KIND_DETACHED { - return Err(DecodeError::InvalidTag); - } - let exec_id = r.get_str()?; - let slot = r.get_u32()?; - let boot_id = r.get_str()?; - let create_time_unix = r.get_u64()?; - let dispatch_deadline_unix = r.get_u64()?; - let argv_sha256 = r.get_str()?; - let state = RecordState::from_tag(r.get_u8()?)?; - let exit_code = get_opt_i32(&mut r)?; - let term_signal = get_opt_u32(&mut r)?; - let lost = r.get_bool()?; - let terminal_time_unix = get_opt_u64(&mut r)?; - r.finish()?; - Ok(Self { - exec_id, - slot, - boot_id, - create_time_unix, - dispatch_deadline_unix, - argv_sha256, - state, - exit_code, - term_signal, - lost, - terminal_time_unix, - }) - } -} - -/// Runner-written lifecycle phase marker. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StatusPhase { - /// Child spawned successfully; supervisor is resident. - Started, - /// Spawn failed (ENOENT/EACCES/exec-format/...). Terminal, retained. - SpawnFailed, - /// Spec-parse / dir / log-open infra failure. The runner did NOT spawn; - /// guestd treats this as a create error and cleans up. - InfraFailed, - /// Child exited with a status code. - Exited(i32), - /// Child terminated by a signal. - Signaled(i32), - /// Cancel/stop path completed (TERM->grace->KILL->reap). - Cancelled, -} - -impl StatusPhase { - pub fn is_terminal(self) -> bool { - matches!( - self, - StatusPhase::SpawnFailed - | StatusPhase::InfraFailed - | StatusPhase::Exited(_) - | StatusPhase::Signaled(_) - | StatusPhase::Cancelled - ) - } - - fn tag(self) -> u8 { - match self { - StatusPhase::Started => 0, - StatusPhase::SpawnFailed => 1, - StatusPhase::InfraFailed => 2, - StatusPhase::Exited(_) => 3, - StatusPhase::Signaled(_) => 4, - StatusPhase::Cancelled => 5, - } - } -} - -/// Wrapper carrying the phase (kept distinct from [`StatusPhase`] so future -/// status metadata can be added without churning call sites). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct StatusRecord { - pub phase: StatusPhase, -} - -impl StatusRecord { - pub fn new(phase: StatusPhase) -> Self { - Self { phase } - } - - pub fn encode(&self) -> Vec { - let mut w = Writer::new(); - w.put_u32(STATUS_MAGIC); - w.put_u32(STATUS_VERSION); - w.put_u8(self.phase.tag()); - match self.phase { - StatusPhase::Exited(code) => w.put_i32(code), - StatusPhase::Signaled(signal) => w.put_i32(signal), - _ => {} - } - w.into_vec() - } - - pub fn decode(bytes: &[u8]) -> Result { - let mut r = Reader::new(bytes); - if r.get_u32()? != STATUS_MAGIC { - return Err(DecodeError::BadMagic); - } - if r.get_u32()? != STATUS_VERSION { - return Err(DecodeError::BadVersion); - } - let tag = r.get_u8()?; - let phase = match tag { - 0 => StatusPhase::Started, - 1 => StatusPhase::SpawnFailed, - 2 => StatusPhase::InfraFailed, - 3 => StatusPhase::Exited(r.get_i32()?), - 4 => StatusPhase::Signaled(r.get_i32()?), - 5 => StatusPhase::Cancelled, - _ => return Err(DecodeError::InvalidTag), - }; - r.finish()?; - Ok(Self { phase }) - } -} - -fn put_opt_i32(w: &mut Writer, value: Option) { - match value { - Some(v) => { - w.put_bool(true); - w.put_i32(v); - } - None => w.put_bool(false), - } -} - -fn put_opt_u32(w: &mut Writer, value: Option) { - match value { - Some(v) => { - w.put_bool(true); - w.put_u32(v); - } - None => w.put_bool(false), - } -} - -fn put_opt_u64(w: &mut Writer, value: Option) { - match value { - Some(v) => { - w.put_bool(true); - w.put_u64(v); - } - None => w.put_bool(false), - } -} - -fn get_opt_i32(r: &mut Reader<'_>) -> Result, DecodeError> { - if r.get_bool()? { - Ok(Some(r.get_i32()?)) - } else { - Ok(None) - } -} - -fn get_opt_u32(r: &mut Reader<'_>) -> Result, DecodeError> { - if r.get_bool()? { - Ok(Some(r.get_u32()?)) - } else { - Ok(None) - } -} - -fn get_opt_u64(r: &mut Reader<'_>) -> Result, DecodeError> { - if r.get_bool()? { - Ok(Some(r.get_u64()?)) - } else { - Ok(None) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn sample_record() -> DurableRecord { - DurableRecord { - exec_id: "00112233445566778899aabbccddeeff".to_owned(), - slot: 3, - boot_id: "boot-xyz".to_owned(), - create_time_unix: 1_700_000_000, - dispatch_deadline_unix: 1_700_000_030, - argv_sha256: "f".repeat(64), - state: RecordState::Running, - exit_code: None, - term_signal: None, - lost: false, - terminal_time_unix: None, - } - } - - #[test] - fn record_round_trips() { - let record = sample_record(); - let bytes = record.encode(); - let decoded = DurableRecord::decode(&bytes).unwrap(); - assert!(record == decoded); - } - - #[test] - fn record_round_trips_terminal() { - let mut record = sample_record(); - record.state = RecordState::Exited; - record.exit_code = Some(42); - record.terminal_time_unix = Some(1_700_000_100); - let decoded = DurableRecord::decode(&record.encode()).unwrap(); - assert!(record == decoded); - assert!(decoded.state.is_terminal()); - - let mut record = sample_record(); - record.state = RecordState::Cancelled; - record.lost = true; - record.terminal_time_unix = Some(1_700_000_200); - let decoded = DurableRecord::decode(&record.encode()).unwrap(); - assert!(record == decoded); - } - - #[test] - fn record_rejects_bad_magic() { - let mut bytes = sample_record().encode(); - bytes[0] ^= 0xff; - assert_eq!(DurableRecord::decode(&bytes), Err(DecodeError::BadMagic)); - } - - #[test] - fn status_round_trips_each_phase() { - for phase in [ - StatusPhase::Started, - StatusPhase::SpawnFailed, - StatusPhase::InfraFailed, - StatusPhase::Exited(7), - StatusPhase::Signaled(9), - StatusPhase::Cancelled, - ] { - let record = StatusRecord::new(phase); - let decoded = StatusRecord::decode(&record.encode()).unwrap(); - assert_eq!(record, decoded); - } - assert!(!StatusPhase::Started.is_terminal()); - assert!(StatusPhase::Exited(0).is_terminal()); - } - - #[test] - fn record_debug_is_redacted() { - let record = sample_record(); - let rendered = format!("{record:?}"); - assert!(!rendered.contains("00112233")); - assert!(!rendered.contains("boot-xyz")); - } -} diff --git a/packages/d2b-exec-runner/src/service_mode.rs b/packages/d2b-exec-runner/src/service_mode.rs deleted file mode 100644 index 51d33e0ed7..0000000000 --- a/packages/d2b-exec-runner/src/service_mode.rs +++ /dev/null @@ -1,1808 +0,0 @@ -//! `d2b-exec-runner --serve-exec --slot NN` service mode. -//! -//! This is the per-slot detached supervisor systemd launches as -//! `d2b-exec-.service`. It reads the SpecCodec spec guestd wrote into -//! the slot dir, spawns the child in its own process group, streams stdout and -//! stderr into the slot's FileRings, and stays resident as supervisor until the -//! child exits or a cancel (control-file sentinel) / optional runtime ceiling -//! fires. It installs NO in-process signal handler (cancellation is polled from -//! the `cancel` control file), so a stop SIGTERM reliably reaches the child. -//! -//! The supervision core is generic over fakeable `Spawner` / `Signaller` / -//! `Clock` / `CancelSource` traits so the cancel/precedence/ceiling matrix is -//! tested deterministically without spawning real processes. Production impls -//! use `std::process` + `rustix` (the binary half of the crate may use rustix; -//! the library half stays dependency-pure). - -use std::io::{ErrorKind, Read}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread::JoinHandle; -use std::time::Duration; - -use d2b_exec_runner::DETACHED_RETAINED_PER_VM; -use d2b_exec_runner::atomicio::{atomic_write, read_file_nofollow}; -use d2b_exec_runner::filering::FileRing; -use d2b_exec_runner::paths::{RunnerPaths, Stream}; -use d2b_exec_runner::record::{StatusPhase, StatusRecord}; -use d2b_exec_runner::spec::{ExecSpec, SpecCodec}; - -/// Drain buffer size (mirrors guestd's PIPE_READ_CHUNK). -const DRAIN_CHUNK: usize = 64 * 1024; - -/// Production control-watcher poll interval. -const DEFAULT_POLL: Duration = Duration::from_millis(100); -/// Production child TERM->KILL grace (must stay well under the unit's -/// `TimeoutStopSec` so systemd's backstop SIGKILL never races the status write). -const DEFAULT_GRACE: Duration = Duration::from_secs(5); - -/// Stop signals the supervisor may send to the child's process group. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StopSignal { - Term, - Kill, -} - -/// Terminal disposition of the direct child. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ChildOutcome { - Exited(i32), - Signaled(i32), -} - -/// Opaque spawn failure. Carries no payload. -#[allow(dead_code)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SpawnFailure { - /// Workload exec failure (retained terminal 127-style result). - SpawnFailed, - /// Runner infrastructure failure (unsafe slot/spec/unit placement). - InfraFailed, -} - -/// A spawned, supervised child. The supervisor takes the stdout/stderr readers -/// for draining and calls `wait` exactly once to reap the direct child. -pub trait ChildHandle: Send { - fn pgid(&self) -> i32; - fn take_stdout(&mut self) -> Option>; - fn take_stderr(&mut self) -> Option>; - fn wait(&mut self) -> ChildOutcome; -} - -/// Spawns the validated spec's command in its own process group. -pub trait Spawner: Send + Sync { - fn spawn(&self, spec: &ExecSpec) -> Result, SpawnFailure>; -} - -/// Signals a child process group (best-effort, idempotent). -pub trait Signaller: Send + Sync { - fn signal_group(&self, pgid: i32, signal: StopSignal); - fn kill_workload_unit(&self, unit_name: &str); -} - -/// Monotonic millisecond clock (fakeable). -pub trait Clock: Send + Sync { - fn now_ms(&self) -> u64; -} - -/// Observes the cancel control-file sentinel (fakeable). -pub trait CancelSource: Send + Sync { - fn is_cancelled(&self) -> bool; -} - -/// Fakeable timing for the control watcher. -#[derive(Debug, Clone, Copy)] -pub struct SuperviseConfig { - pub poll_interval: Duration, - pub grace: Duration, -} - -impl Default for SuperviseConfig { - fn default() -> Self { - Self { - poll_interval: DEFAULT_POLL, - grace: DEFAULT_GRACE, - } - } -} - -/// Outcome of a service-mode run that determines the process exit code. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RunnerResult { - /// The runner did its job (terminal/infra status written). Exit 0; the unit - /// completes cleanly and guestd reads the status file. - Done, - /// The slot dir is so broken the runner could not even write a status file. - /// Exit non-zero so the unit fails and guestd's reconciliation notices. - StatusUnwritable, -} - -fn write_status(paths: &RunnerPaths, phase: StatusPhase) -> Result<(), ()> { - atomic_write(&paths.status(), &StatusRecord::new(phase).encode()).map_err(|_| ()) -} - -fn open_ring(paths: &RunnerPaths, stream: Stream, cap: u64) -> Result { - FileRing::create(&paths.data(stream), &paths.sidecar(stream), cap).map_err(|_| ()) -} - -fn spawn_drain( - mut reader: Box, - ring: Arc>, - done: std::sync::mpsc::Sender<()>, -) -> JoinHandle<()> { - std::thread::spawn(move || { - let mut buf = [0u8; DRAIN_CHUNK]; - let mut lost = false; - loop { - match reader.read(&mut buf) { - Ok(0) => break, - Ok(n) => { - if !lost { - let mut guard = ring.lock().expect("ring poisoned"); - if guard.append(&buf[..n]).is_err() { - // Write error (e.g. /run full): mark the stream lost - // and keep draining so the child never blocks on a - // stuck log writer. - let _ = guard.mark_lost(); - lost = true; - } - } - } - Err(ref err) if err.kind() == ErrorKind::Interrupted => continue, - Err(_) => break, - } - } - { - let mut guard = ring.lock().expect("ring poisoned"); - let _ = guard.mark_eof(); - } - // Signal clean completion so the supervisor can bound its wait. A send - // failure (supervisor already moved on) is benign. - let _ = done.send(()); - }) -} - -/// Wait up to `grace` for the drain threads to finish naturally. In the normal -/// case the direct child's exit closed the only pipe write-ends, so the drains -/// EOF and finish at once (capturing all output + a clean stream EOF). If a -/// leaked descendant inherited a pipe write-end the drains can block -/// indefinitely; this bounded wait then returns so the terminal status is -/// still published and the runner never hangs. Returns the number of drains -/// that finished within the grace. -fn await_drains_bounded( - done_rx: &std::sync::mpsc::Receiver<()>, - expected: usize, - grace: Duration, -) -> usize { - let deadline = std::time::Instant::now() + grace; - let mut finished = 0; - while finished < expected { - let Some(remaining) = deadline.checked_duration_since(std::time::Instant::now()) else { - break; - }; - match done_rx.recv_timeout(remaining) { - Ok(()) => finished += 1, - Err(_) => break, - } - } - finished -} - -/// Supervise one detached exec. Writes spawn-failed/infra-failed/terminal -/// status as appropriate. Generic over the spawn/signal/clock/cancel traits. -#[allow(clippy::too_many_arguments)] -pub fn supervise( - spec: &ExecSpec, - paths: &RunnerPaths, - spawner: &dyn Spawner, - signaller: Arc, - clock: Arc, - cancel: Arc, - cfg: &SuperviseConfig, -) -> RunnerResult { - // Reserve both log rings up front; a failure here is an infra failure - // (guestd treats the infra-failed status as a create error and cleans up). - let stdout_ring = match open_ring(paths, Stream::Stdout, spec.stdout_log_cap) { - Ok(ring) => Arc::new(Mutex::new(ring)), - Err(()) => return finish_infra_failed(paths), - }; - let stderr_ring = match open_ring(paths, Stream::Stderr, spec.stderr_log_cap) { - Ok(ring) => Arc::new(Mutex::new(ring)), - Err(()) => return finish_infra_failed(paths), - }; - - let mut child = match spawner.spawn(spec) { - Ok(child) => child, - Err(SpawnFailure::SpawnFailed) => { - // Legitimate terminal exec: retained, exit 0. - return match write_status(paths, StatusPhase::SpawnFailed) { - Ok(()) => RunnerResult::Done, - Err(()) => RunnerResult::StatusUnwritable, - }; - } - Err(SpawnFailure::InfraFailed) => return finish_infra_failed(paths), - }; - - let pgid = child.pgid(); - let start_ms = clock.now_ms(); - let ceiling_ms = spec.max_runtime_sec.saturating_mul(1_000); - - if write_status(paths, StatusPhase::Started).is_err() { - // The child is live but we cannot publish `started`; tear it down and - // fail the unit so reconciliation cleans up. - signaller.signal_group(pgid, StopSignal::Kill); - signaller.kill_workload_unit(&spec.workload_unit_name); - let _ = child.wait(); - return RunnerResult::StatusUnwritable; - } - - // Drain stdout/stderr concurrently so the child never blocks on a full pipe. - // Each drain signals clean completion on `done_tx`; the supervisor uses it - // to bound its post-reap wait so a leaked descendant holding a pipe - // write-end can never stall the terminal status. - let (done_tx, done_rx) = std::sync::mpsc::channel(); - let mut drains = Vec::new(); - if let Some(reader) = child.take_stdout() { - drains.push(spawn_drain( - reader, - Arc::clone(&stdout_ring), - done_tx.clone(), - )); - } - if let Some(reader) = child.take_stderr() { - drains.push(spawn_drain( - reader, - Arc::clone(&stderr_ring), - done_tx.clone(), - )); - } - let drain_count = drains.len(); - // Drop our own sender so the channel disconnects once every drain thread - // has finished (lets `recv_timeout` observe disconnect promptly). - drop(done_tx); - - let reaped = Arc::new(AtomicBool::new(false)); - let cancel_requested = Arc::new(AtomicBool::new(false)); - - let watcher = spawn_watcher( - pgid, - start_ms, - ceiling_ms, - *cfg, - Arc::clone(&reaped), - Arc::clone(&cancel_requested), - Arc::clone(&signaller), - Arc::clone(&clock), - Arc::clone(&cancel), - &spec.workload_unit_name, - ); - - // The supervisor is the single reaper. - let outcome = child.wait(); - reaped.store(true, Ordering::SeqCst); - - // The watcher exits as soon as it observes `reaped`; join it so - // `cancel_requested` is final before we decide the terminal phase. - let _ = watcher.join(); - if cancel_requested.load(Ordering::SeqCst) { - signaller.kill_workload_unit(&spec.workload_unit_name); - } - - // Decide the terminal phase NOW, before any (possibly unbounded) wait on the - // drains. Exactly-once terminal status: cancellation (sentinel or ceiling) - // wins iff it was requested before the child was reaped; otherwise the - // natural exit status wins. - let phase = if cancel_requested.load(Ordering::SeqCst) { - StatusPhase::Cancelled - } else { - match outcome { - ChildOutcome::Exited(code) => StatusPhase::Exited(code), - ChildOutcome::Signaled(signal) => StatusPhase::Signaled(signal), - } - }; - - // Best-effort: give the drains a bounded grace to flush their follows and mark - // EOF. In the normal case the child's exit closed the only write-ends and - // they finish immediately. If a leaked descendant inherited a write-end the - // drains may block forever - we stop waiting and publish the terminal status - // anyway. Any still-blocked drain thread is detached (dropping its - // `JoinHandle`) and reclaimed at process exit; the streams simply lack a - // clean EOF, which readers already tolerate. - let _finished = await_drains_bounded(&done_rx, drain_count, cfg.grace); - drop(drains); - - match write_status(paths, phase) { - Ok(()) => RunnerResult::Done, - Err(()) => RunnerResult::StatusUnwritable, - } -} - -fn finish_infra_failed(paths: &RunnerPaths) -> RunnerResult { - match write_status(paths, StatusPhase::InfraFailed) { - Ok(()) => RunnerResult::Done, - Err(()) => RunnerResult::StatusUnwritable, - } -} - -#[allow(clippy::too_many_arguments)] -fn spawn_watcher( - pgid: i32, - start_ms: u64, - ceiling_ms: u64, - cfg: SuperviseConfig, - reaped: Arc, - cancel_requested: Arc, - signaller: Arc, - clock: Arc, - cancel: Arc, - workload_unit_name: &str, -) -> JoinHandle<()> { - let workload_unit_name = workload_unit_name.to_owned(); - let poll = cfg.poll_interval.max(Duration::from_millis(1)); - let grace_steps = { - let grace = cfg.grace.as_millis().max(1); - let poll_ms = poll.as_millis().max(1); - grace.div_ceil(poll_ms) as u32 - }; - std::thread::spawn(move || { - loop { - if reaped.load(Ordering::SeqCst) { - return; - } - let ceiling_hit = - ceiling_ms > 0 && clock.now_ms().saturating_sub(start_ms) >= ceiling_ms; - if cancel.is_cancelled() || ceiling_hit { - cancel_requested.store(true, Ordering::SeqCst); - signaller.signal_group(pgid, StopSignal::Term); - for _ in 0..grace_steps { - if reaped.load(Ordering::SeqCst) { - return; - } - std::thread::sleep(poll); - } - if !reaped.load(Ordering::SeqCst) { - signaller.signal_group(pgid, StopSignal::Kill); - signaller.kill_workload_unit(&workload_unit_name); - } - return; - } - std::thread::sleep(poll); - } - }) -} - -/// Entry point for `--serve-exec --slot NN`. Returns the process exit code. -pub fn main_service(slot: u32) -> i32 { - if slot >= DETACHED_RETAINED_PER_VM as u32 { - eprintln!("d2b-exec-runner: slot out of range"); - return 64; - } - let paths = RunnerPaths::for_slot(slot); - - if validate_slot_dir(&paths).is_err() { - // We cannot trust the slot dir; do not attempt to write status there. - eprintln!("d2b-exec-runner: slot directory validation failed"); - return 71; - } - - let spec = match read_spec(&paths) { - Ok(spec) => spec, - Err(()) => { - return match write_status(&paths, StatusPhase::InfraFailed) { - Ok(()) => 0, - Err(()) => 71, - }; - } - }; - - let systemctl_path = production::sibling_systemctl_path(&spec.systemd_run_path); - let signaller: Arc = - Arc::new(production::RustixSignaller::new(systemctl_path.clone())); - let clock: Arc = Arc::new(production::MonotonicClock::new()); - let cancel: Arc = Arc::new(production::FileCancelSource::new(paths.cancel())); - let cfg = SuperviseConfig::default(); - - match supervise( - &spec, - &paths, - &production::StdSpawner::new(slot, systemctl_path), - signaller, - clock, - cancel, - &cfg, - ) { - RunnerResult::Done => 0, - RunnerResult::StatusUnwritable => 71, - } -} - -fn read_spec(paths: &RunnerPaths) -> Result { - let bytes = read_file_nofollow(&paths.spec()).map_err(|_| ())?; - SpecCodec::decode(&bytes).map_err(|_| ()) -} - -/// Validate the slot dir is a real, root-owned directory reached without -/// traversing a symlink (dir-fd `O_NOFOLLOW` openat on each component). -fn validate_slot_dir(paths: &RunnerPaths) -> Result<(), ()> { - use rustix::fs::{Mode, OFlags, fstat, open, openat}; - - let dir_flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC; - let base = open(paths.base(), dir_flags, Mode::empty()).map_err(|_| ())?; - let base_stat = fstat(&base).map_err(|_| ())?; - if base_stat.st_uid != 0 { - return Err(()); - } - let slot = openat(&base, paths.slot_dir_name(), dir_flags, Mode::empty()).map_err(|_| ())?; - let slot_stat = fstat(&slot).map_err(|_| ())?; - if slot_stat.st_uid != 0 { - return Err(()); - } - Ok(()) -} - -mod production { - use std::os::unix::process::{CommandExt, ExitStatusExt}; - use std::path::{Path, PathBuf}; - use std::process::{Child, Command, Stdio}; - use std::sync::Arc; - use std::time::{Duration, Instant}; - - use rustix::process::{Pid, Signal, kill_process_group}; - - use super::{ - CancelSource, ChildHandle, ChildOutcome, Clock, Signaller, SpawnFailure, Spawner, - StopSignal, - }; - use std::io::Read; - - use d2b_exec_runner::spec::ExecSpec; - - const SYSTEMCTL_KILL_TIMEOUT: Duration = Duration::from_secs(2); - const SLICE_VERIFY_TIMEOUT: Duration = Duration::from_secs(2); - const SLICE_VERIFY_POLL: Duration = Duration::from_millis(50); - - pub(super) fn sibling_systemctl_path(systemd_run_path: &str) -> PathBuf { - Path::new(systemd_run_path) - .parent() - .map(|dir| dir.join("systemctl")) - .unwrap_or_else(|| PathBuf::from("systemctl")) - } - - pub struct StdSpawner { - runner_unit_name: String, - systemctl_path: PathBuf, - passwd_path: PathBuf, - slice_verifier: Arc, - unit_killer: Arc, - } - - impl StdSpawner { - pub fn new(slot: u32, systemctl_path: PathBuf) -> Self { - Self { - runner_unit_name: format!("d2b-exec-{slot:02}.service"), - systemctl_path, - passwd_path: PathBuf::from("/etc/passwd"), - slice_verifier: Arc::new(SystemSliceVerifier), - unit_killer: Arc::new(SystemWorkloadUnitKiller), - } - } - - #[cfg(test)] - pub(super) fn new_for_test( - slot: u32, - systemctl_path: PathBuf, - passwd_path: PathBuf, - slice_verifier: Arc, - unit_killer: Arc, - ) -> Self { - Self { - runner_unit_name: format!("d2b-exec-{slot:02}.service"), - systemctl_path, - passwd_path, - slice_verifier, - unit_killer, - } - } - } - - impl Spawner for StdSpawner { - fn spawn(&self, spec: &ExecSpec) -> Result, SpawnFailure> { - if !verify_never_root_against_passwd(spec, &self.passwd_path) { - return Err(SpawnFailure::InfraFailed); - } - if !workload_argv_is_expected(spec, &self.runner_unit_name) { - return Err(SpawnFailure::InfraFailed); - } - - let mut cmd = Command::new(&spec.systemd_run_path); - cmd.args(&spec.systemd_run_args) - .env_clear() - .current_dir("/") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - // New process group for the systemd-run wrapper. The actual - // workload is killed by its deterministic systemd unit; the PG - // remains the local backstop for the wrapper. - .process_group(0); - let mut child = cmd.spawn().map_err(|_| SpawnFailure::InfraFailed)?; - let pgid = child.id() as i32; - - match self.slice_verifier.verify_expected_slice( - &self.systemctl_path, - &self.runner_unit_name, - &spec.workload_unit_name, - ) { - SliceCheck::Verified => {} - SliceCheck::NotObserved | SliceCheck::Unsafe => { - signal_process_group(pgid, StopSignal::Kill); - self.unit_killer - .kill_workload_unit(&self.systemctl_path, &spec.workload_unit_name); - let _ = child.wait(); - return Err(SpawnFailure::InfraFailed); - } - } - - let stdout = child - .stdout - .take() - .map(|s| Box::new(s) as Box); - let stderr = child - .stderr - .take() - .map(|s| Box::new(s) as Box); - Ok(Box::new(StdChild { - child: Some(child), - pgid, - stdout, - stderr, - systemctl_path: self.systemctl_path.clone(), - workload_unit_name: spec.workload_unit_name.clone(), - })) - } - } - - struct StdChild { - child: Option, - pgid: i32, - stdout: Option>, - stderr: Option>, - systemctl_path: PathBuf, - workload_unit_name: String, - } - - impl ChildHandle for StdChild { - fn pgid(&self) -> i32 { - self.pgid - } - - fn take_stdout(&mut self) -> Option> { - self.stdout.take() - } - - fn take_stderr(&mut self) -> Option> { - self.stderr.take() - } - - fn wait(&mut self) -> ChildOutcome { - match self.child.as_mut().map(Child::wait) { - Some(Ok(status)) => { - if let Some(code) = status.code() { - ChildOutcome::Exited(code) - } else if let Some(signal) = status.signal() { - ChildOutcome::Signaled(signal) - } else { - ChildOutcome::Exited(-1) - } - } - _ => ChildOutcome::Exited(-1), - } - } - } - - impl Drop for StdChild { - fn drop(&mut self) { - systemctl_kill_unit(&self.systemctl_path, &self.workload_unit_name); - } - } - - impl RustixSignaller { - pub fn new(systemctl_path: PathBuf) -> Self { - Self { systemctl_path } - } - } - - pub struct RustixSignaller { - systemctl_path: PathBuf, - } - - impl Signaller for RustixSignaller { - fn signal_group(&self, pgid: i32, signal: StopSignal) { - signal_process_group(pgid, signal); - } - - fn kill_workload_unit(&self, unit_name: &str) { - systemctl_kill_unit(&self.systemctl_path, unit_name); - } - } - - fn signal_process_group(pgid: i32, signal: StopSignal) { - // The group persists while the (possibly zombie) leader is unreaped, so - // the PGID cannot be reused under this signal; the supervisor stops - // signalling once it reaps the leader. - if let Some(pid) = Pid::from_raw(pgid) { - let sig = match signal { - StopSignal::Term => Signal::Term, - StopSignal::Kill => Signal::Kill, - }; - let _ = kill_process_group(pid, sig); - } - } - - pub(super) fn passwd_uid_for(content: &str, user: &str) -> Option { - for line in content.lines() { - let line = line.strip_suffix('\r').unwrap_or(line); - if line.is_empty() || line.starts_with('#') { - continue; - } - let mut fields = line.split(':'); - if fields.next() != Some(user) { - continue; - } - return fields.nth(1).and_then(|uid| uid.parse::().ok()); - } - None - } - - pub(super) fn verify_never_root_with_passwd(spec: &ExecSpec, content: &str) -> bool { - let Some((pre, _post)) = split_systemd_run_args(spec) else { - return false; - }; - let uid_arg = format!("--uid={}", spec.exec_user); - if pre.iter().filter(|arg| *arg == &uid_arg).count() != 1 { - return false; - } - if pre.iter().any(|arg| { - arg == "-p" - || arg == "--property" - || arg.starts_with("--uid=") && arg != &uid_arg - || arg.starts_with("User=") - || arg.starts_with("--property=User=") - || arg.starts_with("-p User=") - }) { - return false; - } - matches!( - passwd_uid_for(content, &spec.exec_user), - Some(uid) if uid != 0 && uid == spec.exec_uid - ) - } - - fn verify_never_root_against_passwd(spec: &ExecSpec, passwd_path: &Path) -> bool { - std::fs::read_to_string(passwd_path) - .map(|content| verify_never_root_with_passwd(spec, &content)) - .unwrap_or(false) - } - - pub(super) fn workload_argv_is_expected(spec: &ExecSpec, runner_unit_name: &str) -> bool { - let Some((pre, post)) = split_systemd_run_args(spec) else { - return false; - }; - let expected_pre = expected_systemd_run_pre_args(spec, runner_unit_name); - let expected_post_prefix = [ - spec.login_shell_path.as_str(), - "-l", - "-c", - r#"exec "$@""#, - "d2b-exec", - ]; - pre == expected_pre.as_slice() - && post.len() == expected_post_prefix.len() + spec.argv.len() - && post - .iter() - .take(expected_post_prefix.len()) - .map(String::as_str) - .eq(expected_post_prefix) - && &post[expected_post_prefix.len()..] == spec.argv.as_slice() - } - - fn expected_systemd_run_pre_args(spec: &ExecSpec, runner_unit_name: &str) -> Vec { - let mut expected = vec![ - format!("--uid={}", spec.exec_user), - format!("--unit={}", spec.workload_unit_name), - "--quiet".to_owned(), - "--collect".to_owned(), - "--expand-environment=no".to_owned(), - "--slice=d2b-exec.slice".to_owned(), - "--pipe".to_owned(), - "--wait".to_owned(), - "--property=PAMName=login".to_owned(), - format!("--property=BindsTo={runner_unit_name}"), - format!("--property=PartOf={runner_unit_name}"), - format!("--property=After={runner_unit_name}"), - format!("--working-directory={}", spec.cwd.as_deref().unwrap_or("/")), - ]; - for env in &spec.env { - expected.push("--setenv".to_owned()); - expected.push(format!("{}={}", env.key, env.value)); - } - expected - } - - fn split_systemd_run_args(spec: &ExecSpec) -> Option<(&[String], &[String])> { - let sep = spec.systemd_run_args.iter().position(|arg| arg == "--")?; - Some(( - &spec.systemd_run_args[..sep], - &spec.systemd_run_args[sep + 1..], - )) - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub(super) enum SliceCheck { - Verified, - NotObserved, - Unsafe, - } - - pub(super) trait SliceVerifier: Send + Sync { - fn verify_expected_slice( - &self, - systemctl_path: &Path, - runner_unit_name: &str, - workload_unit_name: &str, - ) -> SliceCheck; - } - - struct SystemSliceVerifier; - - impl SliceVerifier for SystemSliceVerifier { - fn verify_expected_slice( - &self, - systemctl_path: &Path, - runner_unit_name: &str, - workload_unit_name: &str, - ) -> SliceCheck { - verify_expected_slice_with_retry(systemctl_path, runner_unit_name, workload_unit_name) - } - } - - pub(super) trait WorkloadUnitKiller: Send + Sync { - fn kill_workload_unit(&self, systemctl_path: &Path, unit_name: &str); - } - - struct SystemWorkloadUnitKiller; - - impl WorkloadUnitKiller for SystemWorkloadUnitKiller { - fn kill_workload_unit(&self, systemctl_path: &Path, unit_name: &str) { - systemctl_kill_unit(systemctl_path, unit_name); - } - } - - fn verify_expected_slice_with_retry( - systemctl_path: &Path, - runner_unit_name: &str, - workload_unit_name: &str, - ) -> SliceCheck { - let deadline = Instant::now() + SLICE_VERIFY_TIMEOUT; - loop { - match query_expected_slice(systemctl_path, runner_unit_name, workload_unit_name) { - SliceCheck::Verified => return SliceCheck::Verified, - SliceCheck::Unsafe => return SliceCheck::Unsafe, - SliceCheck::NotObserved => {} - } - if Instant::now() >= deadline { - return SliceCheck::NotObserved; - } - std::thread::sleep(SLICE_VERIFY_POLL); - } - } - - fn query_expected_slice( - systemctl_path: &Path, - runner_unit_name: &str, - workload_unit_name: &str, - ) -> SliceCheck { - let Ok(output) = Command::new(systemctl_path) - .arg("show") - .arg("--no-pager") - .arg("--property=Id,Slice,ControlGroup") - .arg(runner_unit_name) - .arg(workload_unit_name) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - else { - return SliceCheck::NotObserved; - }; - if !output.status.success() { - return SliceCheck::NotObserved; - } - let text = String::from_utf8_lossy(&output.stdout); - combined_slice_check(&text, runner_unit_name, workload_unit_name) - } - - #[cfg(test)] - pub(super) fn units_in_expected_slice( - show_text: &str, - runner_unit_name: &str, - workload_unit_name: &str, - ) -> bool { - combined_slice_check(show_text, runner_unit_name, workload_unit_name) - == SliceCheck::Verified - } - - fn combined_slice_check( - show_text: &str, - runner_unit_name: &str, - workload_unit_name: &str, - ) -> SliceCheck { - match ( - show_entry_slice_check(show_text, runner_unit_name), - show_entry_slice_check(show_text, workload_unit_name), - ) { - (SliceCheck::Unsafe, _) | (_, SliceCheck::Unsafe) => SliceCheck::Unsafe, - (SliceCheck::Verified, SliceCheck::Verified) => SliceCheck::Verified, - _ => SliceCheck::NotObserved, - } - } - - fn show_entry_slice_check(show_text: &str, unit_name: &str) -> SliceCheck { - for block in show_text.split("\n\n") { - let mut id = None; - let mut slice = None; - let mut cgroup = None; - for line in block.lines() { - if let Some(v) = line.strip_prefix("Id=") { - id = Some(v.trim()); - } else if let Some(v) = line.strip_prefix("Slice=") { - slice = Some(v.trim()); - } else if let Some(v) = line.strip_prefix("ControlGroup=") { - cgroup = Some(v.trim()); - } - } - if id == Some(unit_name) { - if slice != Some("d2b-exec.slice") { - return SliceCheck::Unsafe; - } - // systemd nests `d2b-exec.slice` under `d2b.slice` - // (a `-`-keyed slice name denotes cgroup hierarchy), so the - // live `ControlGroup` is `/d2b.slice/d2b-exec.slice/ - // `, NOT a top-level `/d2b-exec.slice/`. Verify - // the IMMEDIATE parent slice segment is `d2b-exec.slice` - // regardless of ancestor slices; the authoritative `Slice=` - // property above already pins the slice unit assignment. - let expected_suffix = format!("/d2b-exec.slice/{unit_name}"); - return match cgroup { - Some("") => SliceCheck::NotObserved, - Some(value) if value.ends_with(&expected_suffix) => SliceCheck::Verified, - Some(_) => SliceCheck::Unsafe, - None => SliceCheck::NotObserved, - }; - } - } - SliceCheck::NotObserved - } - - fn systemctl_kill_unit(systemctl_path: &Path, unit_name: &str) { - let mut child = match Command::new(systemctl_path) - .arg("--system") - .arg("--no-ask-password") - .arg("--quiet") - .arg("--kill-whom=all") - .arg("--signal=SIGKILL") - .arg("kill") - .arg(unit_name) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - { - Ok(child) => child, - Err(_) => return, - }; - let deadline = Instant::now() + SYSTEMCTL_KILL_TIMEOUT; - loop { - match child.try_wait() { - Ok(Some(_)) => return, - Ok(None) if Instant::now() < deadline => { - std::thread::sleep(Duration::from_millis(10)); - } - Ok(None) => { - let _ = child.kill(); - let _ = child.wait(); - return; - } - Err(_) => return, - } - } - } - - pub struct MonotonicClock { - start: Instant, - } - - impl MonotonicClock { - pub fn new() -> Self { - Self { - start: Instant::now(), - } - } - } - - impl Clock for MonotonicClock { - fn now_ms(&self) -> u64 { - self.start.elapsed().as_millis() as u64 - } - } - - pub struct FileCancelSource { - path: PathBuf, - } - - impl FileCancelSource { - pub fn new(path: PathBuf) -> Self { - Self { path } - } - } - - impl CancelSource for FileCancelSource { - fn is_cancelled(&self) -> bool { - // symlink_metadata does not follow a symlink at the final - // component; presence of the sentinel is the cancel signal. - std::fs::symlink_metadata(&self.path).is_ok() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Cursor; - use std::path::PathBuf; - use std::sync::{Condvar, Mutex as StdMutex}; - - use d2b_exec_runner::RunnerEnv; - use d2b_exec_runner::filering::FileRingReader; - - fn scratch_slot() -> (PathBuf, RunnerPaths) { - // Always place test scratch under the system temp dir (respects TMPDIR, - // falls back to /tmp) - never the repo-relative "." which leaks - // runner-svc-* dirs into the worktree. - let base = std::env::temp_dir(); - let dir = base.join(format!( - "runner-svc-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let paths = RunnerPaths::new(&dir, 3); - std::fs::create_dir_all(paths.slot_dir()).unwrap(); - (dir, paths) - } - - fn spec(argv0: &str, max_runtime_sec: u64) -> ExecSpec { - ExecSpec { - argv: vec![argv0.to_owned()], - cwd: Some("/".to_owned()), - env: Vec::::new(), - stdout_log_cap: 64 * 1024, - stderr_log_cap: 64 * 1024, - max_runtime_sec, - exec_user: "alice".to_owned(), - exec_uid: 1000, - systemd_run_path: "/run/current-system/sw/bin/systemd-run".to_owned(), - login_shell_path: "/run/current-system/sw/bin/bash".to_owned(), - workload_unit_name: "d2b-exec-03-w.service".to_owned(), - systemd_run_args: vec![ - "--uid=alice".to_owned(), - "--unit=d2b-exec-03-w.service".to_owned(), - "--quiet".to_owned(), - "--collect".to_owned(), - "--expand-environment=no".to_owned(), - "--slice=d2b-exec.slice".to_owned(), - "--pipe".to_owned(), - "--wait".to_owned(), - "--property=PAMName=login".to_owned(), - "--property=BindsTo=d2b-exec-03.service".to_owned(), - "--property=PartOf=d2b-exec-03.service".to_owned(), - "--property=After=d2b-exec-03.service".to_owned(), - "--working-directory=/".to_owned(), - "--".to_owned(), - "/run/current-system/sw/bin/bash".to_owned(), - "-l".to_owned(), - "-c".to_owned(), - r#"exec "$@""#.to_owned(), - "d2b-exec".to_owned(), - argv0.to_owned(), - ], - } - } - - fn fast_cfg() -> SuperviseConfig { - SuperviseConfig { - poll_interval: Duration::from_millis(1), - grace: Duration::from_millis(50), - } - } - - fn read_phase(paths: &RunnerPaths) -> StatusPhase { - let bytes = read_file_nofollow(&paths.status()).unwrap(); - StatusRecord::decode(&bytes).unwrap().phase - } - - fn read_stream_until_eof( - paths: &RunnerPaths, - stream: Stream, - ) -> d2b_exec_runner::filering::RingChunk { - let reader = FileRingReader::open(&paths.data(stream), &paths.sidecar(stream)).unwrap(); - let deadline = std::time::Instant::now() + Duration::from_secs(1); - loop { - let chunk = reader.read(0, 1024).unwrap(); - if chunk.eof || std::time::Instant::now() >= deadline { - return chunk; - } - std::thread::sleep(Duration::from_millis(1)); - } - } - - #[test] - fn runner_never_root_guard_re_resolves_exact_uid_arg() { - let good = spec("id", 0); - let passwd = "root:x:0:0:root:/root:/bin/sh\nalice:x:1000:100::/home/alice:/bin/sh\n"; - assert!(production::verify_never_root_with_passwd(&good, passwd)); - - let root_alias = "root:x:0:0:root:/root:/bin/sh\nalice:x:0:0::/home/alice:/bin/sh\n"; - assert!(!production::verify_never_root_with_passwd( - &good, root_alias - )); - - let unresolved = "root:x:0:0:root:/root:/bin/sh\n"; - assert!(!production::verify_never_root_with_passwd( - &good, unresolved - )); - - let mut mismatched_uid = good.clone(); - mismatched_uid.exec_uid = 1001; - assert!(!production::verify_never_root_with_passwd( - &mismatched_uid, - passwd - )); - - let mut wrong_uid_arg = good; - wrong_uid_arg.systemd_run_args[0] = "--uid=bob".to_owned(); - assert!(!production::verify_never_root_with_passwd( - &wrong_uid_arg, - passwd - )); - - let mut post_separator_uid = spec("id", 0); - post_separator_uid.systemd_run_args[0] = "--quiet".to_owned(); - post_separator_uid - .systemd_run_args - .push("--uid=alice".to_owned()); - assert!(!production::verify_never_root_with_passwd( - &post_separator_uid, - passwd - )); - } - - #[test] - fn workload_argv_validation_rejects_pre_separator_overrides() { - let good = spec("id", 0); - assert!(production::workload_argv_is_expected( - &good, - "d2b-exec-03.service" - )); - - let mut duplicate_uid = good.clone(); - let sep = duplicate_uid - .systemd_run_args - .iter() - .position(|arg| arg == "--") - .unwrap(); - duplicate_uid - .systemd_run_args - .insert(sep, "--uid=root".to_owned()); - assert!(!production::workload_argv_is_expected( - &duplicate_uid, - "d2b-exec-03.service" - )); - - let mut root_property = good.clone(); - let sep = root_property - .systemd_run_args - .iter() - .position(|arg| arg == "--") - .unwrap(); - root_property - .systemd_run_args - .insert(sep, "--property=User=root".to_owned()); - assert!(!production::workload_argv_is_expected( - &root_property, - "d2b-exec-03.service" - )); - - let mut reordered = good; - reordered.systemd_run_args.swap(2, 5); - assert!(!production::workload_argv_is_expected( - &reordered, - "d2b-exec-03.service" - )); - } - - #[test] - fn slice_verification_requires_runner_and_workload_in_exec_slice() { - // Production reality: systemd nests `d2b-exec.slice` under - // `d2b.slice`, so the live `ControlGroup` paths carry the - // `/d2b.slice/d2b-exec.slice/` prefix. - let ok = "\ -Id=d2b-exec-03.service -Slice=d2b-exec.slice -ControlGroup=/d2b.slice/d2b-exec.slice/d2b-exec-03.service - -Id=d2b-exec-03-w.service -Slice=d2b-exec.slice -ControlGroup=/d2b.slice/d2b-exec.slice/d2b-exec-03-w.service -"; - assert!(production::units_in_expected_slice( - ok, - "d2b-exec-03.service", - "d2b-exec-03-w.service" - )); - - // A flat (top-level) slice placement must also verify - the check - // anchors on the immediate parent slice segment, not on a fixed - // ancestor prefix. - let flat = "\ -Id=d2b-exec-03.service -Slice=d2b-exec.slice -ControlGroup=/d2b-exec.slice/d2b-exec-03.service - -Id=d2b-exec-03-w.service -Slice=d2b-exec.slice -ControlGroup=/d2b-exec.slice/d2b-exec-03-w.service -"; - assert!(production::units_in_expected_slice( - flat, - "d2b-exec-03.service", - "d2b-exec-03-w.service" - )); - - let bad_workload_slice = ok.replace( - "Slice=d2b-exec.slice\nControlGroup=/d2b.slice/d2b-exec.slice/d2b-exec-03-w.service", - "Slice=user-1000.slice\nControlGroup=/user.slice/user-1000.slice/session-2.scope", - ); - assert!(!production::units_in_expected_slice( - &bad_workload_slice, - "d2b-exec-03.service", - "d2b-exec-03-w.service" - )); - - // A workload whose immediate parent slice is NOT `d2b-exec.slice` - // (even if `d2b-exec.slice` appears as a non-adjacent ancestor) - // must fail closed. - let wrong_parent = "\ -Id=d2b-exec-03.service -Slice=d2b-exec.slice -ControlGroup=/d2b.slice/d2b-exec.slice/d2b-exec-03.service - -Id=d2b-exec-03-w.service -Slice=d2b-exec.slice -ControlGroup=/d2b.slice/d2b-exec.slice/evil.slice/d2b-exec-03-w.service -"; - assert!(!production::units_in_expected_slice( - wrong_parent, - "d2b-exec-03.service", - "d2b-exec-03-w.service" - )); - - let missing_workload = "\ -Id=d2b-exec-03.service -Slice=d2b-exec.slice -ControlGroup=/d2b.slice/d2b-exec.slice/d2b-exec-03.service -"; - assert!(!production::units_in_expected_slice( - missing_workload, - "d2b-exec-03.service", - "d2b-exec-03-w.service" - )); - } - - struct FixedSliceVerifier(production::SliceCheck); - - impl production::SliceVerifier for FixedSliceVerifier { - fn verify_expected_slice( - &self, - _systemctl_path: &std::path::Path, - _runner_unit_name: &str, - _workload_unit_name: &str, - ) -> production::SliceCheck { - self.0 - } - } - - #[derive(Default)] - struct RecordingUnitKiller { - killed: StdMutex>, - } - - impl production::WorkloadUnitKiller for RecordingUnitKiller { - fn kill_workload_unit(&self, _systemctl_path: &std::path::Path, unit_name: &str) { - self.killed.lock().unwrap().push(unit_name.to_owned()); - } - } - - #[test] - fn std_spawner_fails_closed_when_slice_not_verified_even_if_wrapper_exited() { - for check in [ - production::SliceCheck::NotObserved, - production::SliceCheck::Unsafe, - ] { - let (dir, _paths) = scratch_slot(); - let passwd = dir.join("passwd"); - std::fs::write( - &passwd, - "root:x:0:0:root:/root:/bin/sh\nalice:x:1000:100::/home/alice:/bin/sh\n", - ) - .unwrap(); - let killer = Arc::new(RecordingUnitKiller::default()); - let spawner = production::StdSpawner::new_for_test( - 3, - PathBuf::from("/bin/false"), - passwd, - Arc::new(FixedSliceVerifier(check)), - killer.clone(), - ); - let mut spec = spec("/bin/true", 0); - spec.systemd_run_path = std::env::current_exe() - .unwrap() - .to_string_lossy() - .into_owned(); - - let result = spawner.spawn(&spec); - - match result { - Err(error) => assert_eq!(error, SpawnFailure::InfraFailed), - Ok(_) => panic!("StdSpawner accepted unverified slice state {check:?}"), - } - let killed = killer.killed.lock().unwrap().clone(); - assert_eq!( - killed, - vec!["d2b-exec-03-w.service".to_owned()], - "workload unit must be killed on {check:?}" - ); - std::fs::remove_dir_all(&dir).ok(); - } - } - - // ---- Fakes ----------------------------------------------------------- - - #[derive(Default)] - struct FakeState { - signals: Vec, - killed_units: Vec, - exit: Option, - /// When set, receiving this signal causes the child to exit. - die_on: Option, - die_outcome: Option, - } - - struct FakeProc { - state: StdMutex, - cv: Condvar, - } - - impl FakeProc { - fn new(die_on: Option) -> Arc { - Arc::new(Self { - state: StdMutex::new(FakeState { - die_on, - die_outcome: Some(ChildOutcome::Signaled(15)), - ..FakeState::default() - }), - cv: Condvar::new(), - }) - } - - fn set_exit(&self, outcome: ChildOutcome) { - let mut state = self.state.lock().unwrap(); - if state.exit.is_none() { - state.exit = Some(outcome); - } - self.cv.notify_all(); - } - - fn signals(&self) -> Vec { - self.state.lock().unwrap().signals.clone() - } - fn killed_units(&self) -> Vec { - self.state.lock().unwrap().killed_units.clone() - } - } - - struct FakeChild { - proc: Arc, - stdout: Option>, - stderr: Option>, - } - - impl ChildHandle for FakeChild { - fn pgid(&self) -> i32 { - 4242 - } - fn take_stdout(&mut self) -> Option> { - self.stdout.take() - } - fn take_stderr(&mut self) -> Option> { - self.stderr.take() - } - fn wait(&mut self) -> ChildOutcome { - let mut state = self.proc.state.lock().unwrap(); - while state.exit.is_none() { - state = self.proc.cv.wait(state).unwrap(); - } - state.exit.unwrap() - } - } - - struct FakeSpawner { - proc: Arc, - stdout: Vec, - stderr: Vec, - fail: bool, - } - - impl Spawner for FakeSpawner { - fn spawn(&self, _spec: &ExecSpec) -> Result, SpawnFailure> { - if self.fail { - return Err(SpawnFailure::SpawnFailed); - } - Ok(Box::new(FakeChild { - proc: Arc::clone(&self.proc), - stdout: Some(Box::new(Cursor::new(self.stdout.clone()))), - stderr: Some(Box::new(Cursor::new(self.stderr.clone()))), - })) - } - } - - struct InfraFailSpawner; - - impl Spawner for InfraFailSpawner { - fn spawn(&self, _spec: &ExecSpec) -> Result, SpawnFailure> { - Err(SpawnFailure::InfraFailed) - } - } - - struct FakeSignaller { - proc: Arc, - } - - impl Signaller for FakeSignaller { - fn signal_group(&self, _pgid: i32, signal: StopSignal) { - let mut state = self.proc.state.lock().unwrap(); - state.signals.push(signal); - if state.die_on == Some(signal) && state.exit.is_none() { - state.exit = state.die_outcome; - self.proc.cv.notify_all(); - } - } - fn kill_workload_unit(&self, unit_name: &str) { - self.proc - .state - .lock() - .unwrap() - .killed_units - .push(unit_name.to_owned()); - } - } - - struct FixedClock { - now: Arc, - } - use std::sync::atomic::AtomicU64; - - impl Clock for FixedClock { - fn now_ms(&self) -> u64 { - self.now.load(Ordering::SeqCst) - } - } - - struct StepClock { - calls: AtomicU64, - } - - impl Clock for StepClock { - fn now_ms(&self) -> u64 { - if self.calls.fetch_add(1, Ordering::SeqCst) == 0 { - 0 - } else { - 2_000 - } - } - } - - struct FlagCancel { - flag: Arc, - } - - impl CancelSource for FlagCancel { - fn is_cancelled(&self) -> bool { - self.flag.load(Ordering::SeqCst) - } - } - - /// Spawner whose child stdout is the read end of a real pipe. The test - /// keeps the write end open to simulate a leaked descendant that inherited - /// the pipe, so the drain thread can never observe EOF. - struct PipeStdoutSpawner { - proc: Arc, - read_fd: StdMutex>, - } - - impl Spawner for PipeStdoutSpawner { - fn spawn(&self, _spec: &ExecSpec) -> Result, SpawnFailure> { - let fd = self - .read_fd - .lock() - .unwrap() - .take() - .expect("PipeStdoutSpawner spawned more than once"); - let file = std::fs::File::from(fd); - Ok(Box::new(FakeChild { - proc: Arc::clone(&self.proc), - stdout: Some(Box::new(file)), - stderr: None, - })) - } - } - - // ---- Tests ----------------------------------------------------------- - - #[test] - fn natural_exit_captures_output_and_records_exit_code() { - let (dir, paths) = scratch_slot(); - let proc = FakeProc::new(None); - proc.set_exit(ChildOutcome::Exited(7)); - let spawner = FakeSpawner { - proc: Arc::clone(&proc), - stdout: b"hello stdout".to_vec(), - stderr: b"hello stderr".to_vec(), - fail: false, - }; - let result = supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: Arc::clone(&proc), - }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - assert_eq!(result, RunnerResult::Done); - assert_eq!(read_phase(&paths), StatusPhase::Exited(7)); - - let out = read_stream_until_eof(&paths, Stream::Stdout); - assert_eq!(out.data, b"hello stdout"); - assert!(out.eof); - let err = read_stream_until_eof(&paths, Stream::Stderr); - assert_eq!(err.data, b"hello stderr"); - assert!(err.eof); - assert!(proc.signals().is_empty(), "no signals on a natural exit"); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn signaled_child_records_signal() { - let (dir, paths) = scratch_slot(); - let proc = FakeProc::new(None); - proc.set_exit(ChildOutcome::Signaled(9)); - let spawner = FakeSpawner { - proc: Arc::clone(&proc), - stdout: Vec::new(), - stderr: Vec::new(), - fail: false, - }; - supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: Arc::clone(&proc), - }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - assert_eq!(read_phase(&paths), StatusPhase::Signaled(9)); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn spawn_failure_is_terminal_and_retained() { - let (dir, paths) = scratch_slot(); - let proc = FakeProc::new(None); - let spawner = FakeSpawner { - proc, - stdout: Vec::new(), - stderr: Vec::new(), - fail: true, - }; - let result = supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: FakeProc::new(None), - }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - assert_eq!(result, RunnerResult::Done); - assert_eq!(read_phase(&paths), StatusPhase::SpawnFailed); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn infra_spawn_failure_records_infra_failed() { - let (dir, paths) = scratch_slot(); - let proc = FakeProc::new(None); - let result = supervise( - &spec("/bin/true", 0), - &paths, - &InfraFailSpawner, - Arc::new(FakeSignaller { proc }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - assert_eq!(result, RunnerResult::Done); - assert_eq!(read_phase(&paths), StatusPhase::InfraFailed); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn cancel_sentinel_terminates_and_records_cancelled() { - let (dir, paths) = scratch_slot(); - // The child ignores TERM and only dies on KILL, proving TERM is sent - // before the KILL backstop. - let proc = FakeProc::new(Some(StopSignal::Kill)); - let spawner = FakeSpawner { - proc: Arc::clone(&proc), - stdout: Vec::new(), - stderr: Vec::new(), - fail: false, - }; - let flag = Arc::new(AtomicBool::new(true)); - supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: Arc::clone(&proc), - }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { flag }), - &fast_cfg(), - ); - assert_eq!(read_phase(&paths), StatusPhase::Cancelled); - let signals = proc.signals(); - assert_eq!(signals.first(), Some(&StopSignal::Term)); - assert!( - signals.contains(&StopSignal::Kill), - "KILL backstop fires when TERM is ignored: {signals:?}" - ); - assert!( - proc.killed_units() - .contains(&"d2b-exec-03-w.service".to_owned()) - ); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn cancel_when_child_terms_promptly_still_kills_workload_unit() { - let (dir, paths) = scratch_slot(); - // The child dies on TERM, so no KILL backstop is needed. - let proc = FakeProc::new(Some(StopSignal::Term)); - let spawner = FakeSpawner { - proc: Arc::clone(&proc), - stdout: Vec::new(), - stderr: Vec::new(), - fail: false, - }; - supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: Arc::clone(&proc), - }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(true)), - }), - &fast_cfg(), - ); - assert_eq!(read_phase(&paths), StatusPhase::Cancelled); - assert_eq!(proc.signals(), vec![StopSignal::Term]); - assert!( - proc.killed_units() - .contains(&"d2b-exec-03-w.service".to_owned()) - ); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn runtime_ceiling_cancels_when_clock_advances() { - let (dir, paths) = scratch_slot(); - let proc = FakeProc::new(Some(StopSignal::Term)); - let spawner = FakeSpawner { - proc: Arc::clone(&proc), - stdout: Vec::new(), - stderr: Vec::new(), - fail: false, - }; - supervise( - &spec("/bin/true", 1), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: Arc::clone(&proc), - }), - Arc::new(StepClock { - calls: AtomicU64::new(0), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - assert_eq!(read_phase(&paths), StatusPhase::Cancelled); - assert!(proc.signals().contains(&StopSignal::Term)); - assert!( - proc.killed_units() - .contains(&"d2b-exec-03-w.service".to_owned()) - ); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn no_ceiling_does_not_cancel_a_long_running_child() { - let (dir, paths) = scratch_slot(); - let proc = FakeProc::new(None); - let spawner = FakeSpawner { - proc: Arc::clone(&proc), - stdout: Vec::new(), - stderr: Vec::new(), - fail: false, - }; - let now = Arc::new(AtomicU64::new(0)); - // Let the watcher poll several times with a huge clock, then exit - // naturally; max_runtime_sec=0 means no ceiling ever fires. - let proc_exit = Arc::clone(&proc); - let bump = std::thread::spawn(move || { - std::thread::sleep(Duration::from_millis(10)); - proc_exit.set_exit(ChildOutcome::Exited(0)); - }); - now.store(u64::MAX / 2, Ordering::SeqCst); - supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: Arc::clone(&proc), - }), - Arc::new(FixedClock { now }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - bump.join().unwrap(); - assert_eq!(read_phase(&paths), StatusPhase::Exited(0)); - assert!(proc.signals().is_empty(), "no ceiling => no signals"); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn infra_failure_when_slot_dir_missing() { - let base = std::env::var_os("TMPDIR") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(".")); - let dir = base.join(format!("runner-missing-{}", std::process::id())); - // Intentionally do NOT create the slot dir; ring create fails. - let paths = RunnerPaths::new(&dir, 5); - let proc = FakeProc::new(None); - let spawner = FakeSpawner { - proc: Arc::clone(&proc), - stdout: Vec::new(), - stderr: Vec::new(), - fail: false, - }; - let result = supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { proc }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - // No status file could be written (no dir) => unit must fail. - assert_eq!(result, RunnerResult::StatusUnwritable); - std::fs::remove_dir_all(&dir).ok(); - } - - #[test] - fn terminal_status_published_even_with_lingering_pipe_holder() { - let (dir, paths) = scratch_slot(); - // A real pipe: the read end becomes the child's stdout, and we hold the - // write end open for the duration of supervise() to mimic a leaked - // descendant that inherited the child's stdout write-end. The drain - // thread can never observe EOF, so an unbounded drain join would hang - // forever and the terminal status would never be published. - let (read_fd, write_fd) = rustix::pipe::pipe().expect("pipe"); - let proc = FakeProc::new(None); - // The direct child exits immediately; only the leaked pipe lingers. - proc.set_exit(ChildOutcome::Exited(0)); - let spawner = PipeStdoutSpawner { - proc: Arc::clone(&proc), - read_fd: StdMutex::new(Some(read_fd)), - }; - let start = std::time::Instant::now(); - let result = supervise( - &spec("/bin/true", 0), - &paths, - &spawner, - Arc::new(FakeSignaller { - proc: Arc::clone(&proc), - }), - Arc::new(FixedClock { - now: Arc::new(AtomicU64::new(0)), - }), - Arc::new(FlagCancel { - flag: Arc::new(AtomicBool::new(false)), - }), - &fast_cfg(), - ); - // The bounded drain wait must let supervise return promptly even though - // the pipe write-end is still held open. - assert!( - start.elapsed() < Duration::from_secs(30), - "supervise hung on a lingering pipe holder" - ); - assert_eq!(result, RunnerResult::Done); - assert_eq!(read_phase(&paths), StatusPhase::Exited(0)); - // Releasing the write end lets the detached drain thread finish cleanly. - drop(write_fd); - std::fs::remove_dir_all(&dir).ok(); - } -} diff --git a/packages/d2b-exec-runner/src/spec.rs b/packages/d2b-exec-runner/src/spec.rs deleted file mode 100644 index 34e4fc25cf..0000000000 --- a/packages/d2b-exec-runner/src/spec.rs +++ /dev/null @@ -1,381 +0,0 @@ -//! The `spec` file: the SpecCodec-encoded command guestd hands to the runner. -//! -//! guestd encodes (writer); the runner decodes (reader). Length-prefixed -//! binary, no serde, validated on both sides via the crate's validation -//! primitives so a corrupt or oversized spec is rejected deterministically. - -use crate::codec::{DecodeError, Reader, Writer}; -use crate::{ - MAX_ARG_LEN, MAX_ARGV, MAX_ENV, RunnerEnv, ValidationError, contains_nul, validate_argv, - validate_cwd, validate_env, -}; - -/// Spec format magic ("D2ES" = d2b Exec Spec) + version. -const SPEC_MAGIC: u32 = 0x4432_4553; -const SPEC_VERSION: u32 = 2; -const MAX_SYSTEMD_RUN_ARGS: usize = (MAX_ENV * 2) + MAX_ARGV + 32; - -/// The validated, sanitized command the runner supervises. -#[derive(Clone, PartialEq, Eq)] -pub struct ExecSpec { - /// Guest command argv. `argv[0]` may be absolute, bare, or relative; the - /// workload user's login shell performs PATH lookup for bare commands. - pub argv: Vec, - /// Validated absolute working directory (None => `/`). - pub cwd: Option, - /// Sanitized environment (no inherited host env). - pub env: Vec, - /// Per-stream retained-log byte cap. - pub stdout_log_cap: u64, - pub stderr_log_cap: u64, - /// Optional runtime ceiling in seconds; 0 means unlimited (no timer). - pub max_runtime_sec: u64, - /// Workload user name guestd resolved as non-root. - pub exec_user: String, - /// UID guestd resolved for `exec_user`; the runner re-resolves and compares - /// immediately before spawn. - pub exec_uid: u32, - /// Absolute path to `systemd-run`. - pub systemd_run_path: String, - /// Absolute path to the workload user's login shell wrapper. - pub login_shell_path: String, - /// Deterministic slot-derived workload unit name. - pub workload_unit_name: String, - /// `systemd-run` argv (excluding the binary path) built by guestd. - pub systemd_run_args: Vec, -} - -// Never echo argv/env/cwd through Debug. -impl std::fmt::Debug for ExecSpec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ExecSpec") - .field("argc", &self.argv.len()) - .field("has_cwd", &self.cwd.is_some()) - .field("env_count", &self.env.len()) - .field("stdout_log_cap", &self.stdout_log_cap) - .field("stderr_log_cap", &self.stderr_log_cap) - .field("max_runtime_sec", &self.max_runtime_sec) - .field("has_exec_user", &(!self.exec_user.is_empty())) - .field("has_systemd_run_path", &(!self.systemd_run_path.is_empty())) - .field("has_login_shell_path", &(!self.login_shell_path.is_empty())) - .field("systemd_run_argc", &self.systemd_run_args.len()) - .finish() - } -} - -/// Typed spec failure. Carries no caller bytes. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SpecError { - Decode(DecodeError), - Validation(ValidationError), - BadMagic, - BadVersion, - LogCapZero, - BadExecUser, - BadExecUid, - BadPath, - BadWorkloadUnitName, - BadSystemdRunArgs, -} - -impl From for SpecError { - fn from(error: DecodeError) -> Self { - SpecError::Decode(error) - } -} - -impl From for SpecError { - fn from(error: ValidationError) -> Self { - SpecError::Validation(error) - } -} - -impl ExecSpec { - fn validate(&self) -> Result<(), SpecError> { - validate_argv(&self.argv)?; - validate_cwd(self.cwd.as_deref())?; - validate_env(&self.env)?; - if self.stdout_log_cap == 0 || self.stderr_log_cap == 0 { - return Err(SpecError::LogCapZero); - } - validate_exec_user(&self.exec_user)?; - if self.exec_uid == 0 { - return Err(SpecError::BadExecUid); - } - validate_abs_path(&self.systemd_run_path)?; - validate_abs_path(&self.login_shell_path)?; - validate_workload_unit_name(&self.workload_unit_name)?; - validate_systemd_run_args(&self.systemd_run_args)?; - Ok(()) - } -} - -fn validate_exec_user(user: &str) -> Result<(), SpecError> { - if user.is_empty() || user.len() > MAX_ARG_LEN || contains_nul(user) { - return Err(SpecError::BadExecUser); - } - Ok(()) -} - -fn validate_abs_path(path: &str) -> Result<(), SpecError> { - if path.is_empty() || path.len() > MAX_ARG_LEN || contains_nul(path) || !path.starts_with('/') { - return Err(SpecError::BadPath); - } - Ok(()) -} - -fn validate_workload_unit_name(unit: &str) -> Result<(), SpecError> { - if unit.is_empty() - || unit.len() > MAX_ARG_LEN - || contains_nul(unit) - || unit.contains('/') - || !unit.starts_with("d2b-exec-") - || !unit.ends_with("-w.service") - || !unit - .bytes() - .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.')) - { - return Err(SpecError::BadWorkloadUnitName); - } - Ok(()) -} - -fn validate_systemd_run_args(args: &[String]) -> Result<(), SpecError> { - if args.is_empty() || args.len() > MAX_SYSTEMD_RUN_ARGS { - return Err(SpecError::BadSystemdRunArgs); - } - for arg in args { - if arg.is_empty() || arg.len() > MAX_ARG_LEN || contains_nul(arg) { - return Err(SpecError::BadSystemdRunArgs); - } - } - Ok(()) -} - -/// Stateless encoder/decoder for [`ExecSpec`]. -pub struct SpecCodec; - -impl SpecCodec { - /// Encode a validated spec. Returns the validation error if the spec is - /// malformed (guestd validates before writing, so this should not fail in - /// practice, but the encoder is fail-closed). - pub fn encode(spec: &ExecSpec) -> Result, SpecError> { - spec.validate()?; - let mut w = Writer::new(); - w.put_u32(SPEC_MAGIC); - w.put_u32(SPEC_VERSION); - w.put_u32(spec.argv.len() as u32); - for arg in &spec.argv { - w.put_str(arg); - } - match &spec.cwd { - Some(cwd) => { - w.put_bool(true); - w.put_str(cwd); - } - None => w.put_bool(false), - } - w.put_u32(spec.env.len() as u32); - for entry in &spec.env { - w.put_str(&entry.key); - w.put_str(&entry.value); - } - w.put_u64(spec.stdout_log_cap); - w.put_u64(spec.stderr_log_cap); - w.put_u64(spec.max_runtime_sec); - w.put_str(&spec.exec_user); - w.put_u32(spec.exec_uid); - w.put_str(&spec.systemd_run_path); - w.put_str(&spec.login_shell_path); - w.put_str(&spec.workload_unit_name); - w.put_u32(spec.systemd_run_args.len() as u32); - for arg in &spec.systemd_run_args { - w.put_str(arg); - } - Ok(w.into_vec()) - } - - /// Decode and re-validate a spec from on-disk bytes. - pub fn decode(bytes: &[u8]) -> Result { - let mut r = Reader::new(bytes); - if r.get_u32()? != SPEC_MAGIC { - return Err(SpecError::BadMagic); - } - if r.get_u32()? != SPEC_VERSION { - return Err(SpecError::BadVersion); - } - let argc = r.get_u32()? as usize; - if argc > crate::MAX_ARGV { - return Err(SpecError::Validation(ValidationError::TooManyArgs)); - } - let mut argv = Vec::with_capacity(argc); - for _ in 0..argc { - argv.push(r.get_str()?); - } - let cwd = if r.get_bool()? { - Some(r.get_str()?) - } else { - None - }; - let env_count = r.get_u32()? as usize; - if env_count > crate::MAX_ENV { - return Err(SpecError::Validation(ValidationError::TooManyEnv)); - } - let mut env = Vec::with_capacity(env_count); - for _ in 0..env_count { - let key = r.get_str()?; - let value = r.get_str()?; - env.push(RunnerEnv { key, value }); - } - let stdout_log_cap = r.get_u64()?; - let stderr_log_cap = r.get_u64()?; - let max_runtime_sec = r.get_u64()?; - let exec_user = r.get_str()?; - let exec_uid = r.get_u32()?; - let systemd_run_path = r.get_str()?; - let login_shell_path = r.get_str()?; - let workload_unit_name = r.get_str()?; - let systemd_argc = r.get_u32()? as usize; - if systemd_argc > MAX_SYSTEMD_RUN_ARGS { - return Err(SpecError::BadSystemdRunArgs); - } - let mut systemd_run_args = Vec::with_capacity(systemd_argc); - for _ in 0..systemd_argc { - systemd_run_args.push(r.get_str()?); - } - r.finish()?; - - let spec = ExecSpec { - argv, - cwd, - env, - stdout_log_cap, - stderr_log_cap, - max_runtime_sec, - exec_user, - exec_uid, - systemd_run_path, - login_shell_path, - workload_unit_name, - systemd_run_args, - }; - spec.validate()?; - Ok(spec) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn sample() -> ExecSpec { - ExecSpec { - argv: vec!["/bin/sleep".to_owned(), "infinity".to_owned()], - cwd: Some("/var/empty".to_owned()), - env: vec![RunnerEnv { - key: "PATH".to_owned(), - value: "/run/current-system/sw/bin".to_owned(), - }], - stdout_log_cap: 4 * 1024 * 1024, - stderr_log_cap: 4 * 1024 * 1024, - max_runtime_sec: 0, - exec_user: "alice".to_owned(), - exec_uid: 1000, - systemd_run_path: "/run/current-system/sw/bin/systemd-run".to_owned(), - login_shell_path: "/run/current-system/sw/bin/bash".to_owned(), - workload_unit_name: "d2b-exec-07-w.service".to_owned(), - systemd_run_args: vec![ - "--uid=alice".to_owned(), - "--unit=d2b-exec-07-w.service".to_owned(), - "--property=PAMName=login".to_owned(), - "--".to_owned(), - "/run/current-system/sw/bin/bash".to_owned(), - "-l".to_owned(), - "-c".to_owned(), - r#"exec "$@""#.to_owned(), - "d2b-exec".to_owned(), - "/bin/sleep".to_owned(), - "infinity".to_owned(), - ], - } - } - - #[test] - fn round_trips() { - let spec = sample(); - let bytes = SpecCodec::encode(&spec).unwrap(); - let decoded = SpecCodec::decode(&bytes).unwrap(); - assert!(spec == decoded); - } - - #[test] - fn round_trips_without_cwd_and_with_ceiling() { - let mut spec = sample(); - spec.cwd = None; - spec.max_runtime_sec = 600; - let bytes = SpecCodec::encode(&spec).unwrap(); - let decoded = SpecCodec::decode(&bytes).unwrap(); - assert!(spec == decoded); - } - - #[test] - fn rejects_bad_magic_and_version() { - let mut bytes = SpecCodec::encode(&sample()).unwrap(); - bytes[0] ^= 0xff; - assert_eq!(SpecCodec::decode(&bytes), Err(SpecError::BadMagic)); - - let mut bytes = SpecCodec::encode(&sample()).unwrap(); - // Flip the version word (bytes 4..8). - bytes[4] ^= 0xff; - assert_eq!(SpecCodec::decode(&bytes), Err(SpecError::BadVersion)); - } - - #[test] - fn rejects_zero_log_cap_and_invalid_command() { - let mut spec = sample(); - spec.stdout_log_cap = 0; - assert_eq!(SpecCodec::encode(&spec), Err(SpecError::LogCapZero)); - - let mut spec = sample(); - spec.argv = vec!["relative".to_owned()]; - // argv validation here only checks shape (nul/len/leading dash); - // abs-path is deliberately NOT required because detached commands run - // through the workload user's login shell and may be bare/relative. - assert!(SpecCodec::encode(&spec).is_ok()); - - let mut spec = sample(); - spec.argv = vec!["-bad".to_owned()]; - assert_eq!( - SpecCodec::encode(&spec), - Err(SpecError::Validation(ValidationError::ArgLeadingDash)) - ); - - let mut spec = sample(); - spec.argv = vec!["bad\0arg".to_owned()]; - assert_eq!( - SpecCodec::encode(&spec), - Err(SpecError::Validation(ValidationError::ArgContainsNul)) - ); - } - - #[test] - fn decode_rejects_trailing_bytes() { - let mut bytes = SpecCodec::encode(&sample()).unwrap(); - bytes.push(0); - assert_eq!( - SpecCodec::decode(&bytes), - Err(SpecError::Decode(DecodeError::TrailingBytes)) - ); - } - - #[test] - fn debug_never_echoes_payload() { - let spec = sample(); - let rendered = format!("{spec:?}"); - assert!(!rendered.contains("/bin/sleep")); - assert!(!rendered.contains("PATH")); - assert!(!rendered.contains("/run/current-system")); - assert!(!rendered.contains("alice")); - assert!(!rendered.contains("d2b-exec")); - } -} diff --git a/packages/d2b-exec-runner/src/tty_helper.rs b/packages/d2b-exec-runner/src/tty_helper.rs deleted file mode 100644 index 4b264ccef3..0000000000 --- a/packages/d2b-exec-runner/src/tty_helper.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! Interactive TTY exec helper (`--tty-exec`). -//! -//! guestd opens a PTY master/slave pair and spawns this helper with the slave -//! on stdin (fd 0) and an `O_CLOEXEC` status pipe on stdout (fd 1). The helper -//! runs entirely in safe `rustix` and `std` (no `unsafe`, no `pre_exec`): -//! -//! 1. `fcntl(F_DUPFD_CLOEXEC)` the status fd to a high number so the upcoming -//! `dup2` onto fd 1 cannot clobber it and so it auto-closes on a successful -//! `execve`, -//! 2. `setsid()` to become a session leader with no controlling terminal, -//! 3. `ioctl(TIOCSCTTY)` on the slave (fd 0) to acquire it as the controlling -//! terminal of the new session, -//! 4. `tcsetwinsize` the initial rows/cols, -//! 5. `dup2` the slave onto fds 1 and 2 (it is already on fd 0), -//! 6. `execve` the target argv, inheriting the env/cwd guestd set on the helper -//! `Command`. -//! -//! On success the `O_CLOEXEC` status fd closes during `execve`, which guestd -//! observes as EOF on the status pipe's read end. On any setup or `exec` -//! failure the helper writes exactly one typed status byte to that fd and -//! exits; guestd maps the byte to a typed `ExecCreate` error. The helper never -//! logs the target argv and never writes anything to the status fd on success. -//! -//! This module is the binary half of the crate and may use `rustix`; the -//! library half stays dependency-pure. - -use std::os::unix::process::CommandExt; -use std::process::Command; - -use rustix::io::{fcntl_dupfd_cloexec, write}; -use rustix::process::{ioctl_tiocsctty, setsid}; -use rustix::stdio::{dup2_stderr, dup2_stdout, stdin, stdout}; -use rustix::termios::{Winsize, tcsetwinsize}; - -/// Lowest fd the inherited status pipe is duplicated to. Kept clear of the -/// standard fds (0/1/2) so the `dup2` of the slave onto stdout/stderr cannot -/// clobber the status channel. -const STATUS_DUP_MIN_FD: i32 = 10; - -/// Exit code used when the status channel itself is unusable, so the helper -/// cannot signal a typed failure byte. guestd treats a closed status pipe with -/// no byte as a generic helper failure. -const EXIT_NO_STATUS_CHANNEL: i32 = 71; -/// Exit code accompanying any typed failure byte. The byte is authoritative; -/// the exit code is only a fallback for diagnostics. -const EXIT_SETUP_FAILED: i32 = 72; - -/// Typed setup/exec failure. The single status byte guestd reads is the -/// `as_byte()` discriminant; the values are a stable wire contract between the -/// helper and guestd's PTY spawner. guestd maps ANY status byte to a typed -/// `ExecCreate` spawn/setup failure, and only a bare EOF (the `O_CLOEXEC` status -/// fd closing on a successful `execve`) to success - so every non-exec exit path -/// MUST write a byte first, or guestd would misread the exit as success. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum HelperFailure { - /// `setsid()` failed (helper was unexpectedly already a group leader, or - /// the kernel refused). guestd never sets `process_group(0)` on the helper - /// precisely so this cannot happen in the supported path. - Setsid = 1, - /// `ioctl(TIOCSCTTY)` failed: the slave could not be acquired as the - /// controlling terminal. - Ctty = 2, - /// `tcsetwinsize` failed applying the initial geometry. - Winsize = 3, - /// `dup2` of the slave onto stdout/stderr failed. - Dup = 4, - /// `execve` of the target failed (ENOENT/EACCES/ENOEXEC/...). - Exec = 5, - /// Argument parsing failed (malformed `--tty-exec` invocation). Reported - /// over the still-attached status fd (no `dup2` has happened yet). - Args = 6, - /// Duplicating the inherited status fd to a high CLOEXEC fd failed, so the - /// handshake fd could not be preserved across the slave wiring. Reported on - /// the still-attached fd 1 before exit. - StatusDup = 7, -} - -impl HelperFailure { - pub(crate) fn as_byte(self) -> u8 { - self as u8 - } -} - -/// Parsed `--tty-exec --rows R --cols C -- ` arguments. -pub(crate) struct TtyHelperArgs { - pub rows: u16, - pub cols: u16, - pub argv: Vec, -} - -impl TtyHelperArgs { - /// Parse the args that follow `--tty-exec`. Grammar is fixed and - /// order-sensitive: `--rows R --cols C -- ` with a non-empty, - /// absolute argv. Returns `None` on any deviation (fail closed). - pub(crate) fn parse(args: &[String]) -> Option { - let mut rows: Option = None; - let mut cols: Option = None; - let mut idx = 0; - while idx < args.len() { - match args[idx].as_str() { - "--rows" => { - rows = Some(args.get(idx + 1)?.parse().ok()?); - idx += 2; - } - "--cols" => { - cols = Some(args.get(idx + 1)?.parse().ok()?); - idx += 2; - } - "--" => { - idx += 1; - break; - } - _ => return None, - } - } - let argv: Vec = args[idx..].to_vec(); - if argv.is_empty() || !argv[0].starts_with('/') { - return None; - } - Some(Self { - rows: rows?, - cols: cols?, - argv, - }) - } -} - -/// Run the `--tty-exec` helper. `args` are the tokens following `--tty-exec`. -pub(crate) fn run(args: &[String]) -> i32 { - let Some(parsed) = TtyHelperArgs::parse(args) else { - eprintln!("d2b-exec-runner: --tty-exec requires --rows R --cols C -- "); - // fd 1 is still the inherited status pipe write end (no dup2 onto it has - // happened yet). Report a typed byte so guestd does not read the bare - // EOF on helper exit as a successful exec. - let _ = write(stdout(), &[HelperFailure::Args.as_byte()]); - return 64; - }; - // Duplicate the inherited status pipe (fd 1) to a high CLOEXEC fd BEFORE any - // dup2 onto fd 1, so the status channel survives the slave wiring and closes - // on a successful execve (guestd reads EOF == success). - let status = match fcntl_dupfd_cloexec(stdout(), STATUS_DUP_MIN_FD) { - Ok(fd) => fd, - // The dup failed, so we have no high CLOEXEC copy - but fd 1 is still the - // inherited status pipe (the slave wiring has not run). Report a typed - // byte on it before exiting so a bare EOF is never misread as success. - Err(_) => { - let _ = write(stdout(), &[HelperFailure::StatusDup.as_byte()]); - return EXIT_NO_STATUS_CHANNEL; - } - }; - // setup_and_exec only returns on failure; on success it has already replaced - // the process image via execve. - let failure = setup_and_exec(&parsed); - // Best-effort: report the typed byte to guestd over the preserved status fd. - let _ = write(&status, &[failure.as_byte()]); - EXIT_SETUP_FAILED -} - -/// Perform the controlling-terminal handshake and `execve` the target. Returns -/// only on failure (with the typed cause); on success the image is replaced and -/// control never returns here. -fn setup_and_exec(args: &TtyHelperArgs) -> HelperFailure { - // 1. New session, detached from any controlling terminal. - if setsid().is_err() { - return HelperFailure::Setsid; - } - // The PTY slave guestd handed us is on stdin (fd 0). - let slave = stdin(); - // 2. Acquire the slave as the controlling terminal of the new session - // (arg 0: do not steal another session's terminal). - if ioctl_tiocsctty(slave).is_err() { - return HelperFailure::Ctty; - } - // 3. Apply the initial window geometry before the target starts. - let winsize = Winsize { - ws_row: args.rows, - ws_col: args.cols, - ws_xpixel: 0, - ws_ypixel: 0, - }; - if tcsetwinsize(slave, winsize).is_err() { - return HelperFailure::Winsize; - } - // 4. Wire the slave onto stdout + stderr (it is already on stdin). This - // replaces the inherited status pipe on fd 1; the high CLOEXEC copy in - // `run` keeps the status channel alive. - if dup2_stdout(slave).is_err() || dup2_stderr(slave).is_err() { - return HelperFailure::Dup; - } - // 5. Replace the image with the target, inheriting the env/cwd guestd set on - // the helper Command. argv[0] is validated absolute (no PATH search). - let mut cmd = Command::new(&args.argv[0]); - cmd.args(&args.argv[1..]); - let _ = cmd.exec(); - HelperFailure::Exec -} - -#[cfg(test)] -mod tests { - use super::*; - - fn argv(parts: &[&str]) -> Vec { - parts.iter().map(|s| s.to_string()).collect() - } - - #[test] - fn parses_well_formed() { - let a = TtyHelperArgs::parse(&argv(&[ - "--rows", "24", "--cols", "80", "--", "/bin/sh", "-i", - ])) - .expect("parse"); - assert_eq!(a.rows, 24); - assert_eq!(a.cols, 80); - assert_eq!(a.argv, argv(&["/bin/sh", "-i"])); - } - - #[test] - fn rejects_missing_dimensions() { - assert!(TtyHelperArgs::parse(&argv(&["--rows", "24", "--", "/bin/sh"])).is_none()); - assert!(TtyHelperArgs::parse(&argv(&["--cols", "80", "--", "/bin/sh"])).is_none()); - } - - #[test] - fn rejects_relative_or_empty_argv() { - assert!( - TtyHelperArgs::parse(&argv(&["--rows", "24", "--cols", "80", "--", "sh"])).is_none() - ); - assert!(TtyHelperArgs::parse(&argv(&["--rows", "24", "--cols", "80", "--"])).is_none()); - } - - #[test] - fn rejects_unknown_flag() { - assert!( - TtyHelperArgs::parse(&argv(&[ - "--rows", "24", "--cols", "80", "--bogus", "--", "/bin/sh" - ])) - .is_none() - ); - } - - #[test] - fn failure_bytes_are_stable_and_distinct() { - // The byte values are a wire contract with guestd; pin them. - assert_eq!(HelperFailure::Setsid.as_byte(), 1); - assert_eq!(HelperFailure::Ctty.as_byte(), 2); - assert_eq!(HelperFailure::Winsize.as_byte(), 3); - assert_eq!(HelperFailure::Dup.as_byte(), 4); - assert_eq!(HelperFailure::Exec.as_byte(), 5); - assert_eq!(HelperFailure::Args.as_byte(), 6); - assert_eq!(HelperFailure::StatusDup.as_byte(), 7); - } -} diff --git a/packages/d2b-exec-runner/tests/fail_closed.rs b/packages/d2b-exec-runner/tests/fail_closed.rs deleted file mode 100644 index 15e976a56a..0000000000 --- a/packages/d2b-exec-runner/tests/fail_closed.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::process::Command; - -/// The binary fails closed on unknown/no-arg invocations and exposes only the -/// `--version` and `--serve-exec --slot NN` surfaces. -#[test] -fn binary_fails_closed_on_unsupported_invocations() { - let bin = env!("CARGO_BIN_EXE_d2b-exec-runner"); - - let no_args = Command::new(bin).status().expect("run d2b-exec-runner"); - assert_eq!(no_args.code(), Some(78)); - - let unknown = Command::new(bin) - .arg("--unknown") - .status() - .expect("run d2b-exec-runner --unknown"); - assert_eq!(unknown.code(), Some(78)); - - let version = Command::new(bin) - .arg("--version") - .status() - .expect("run d2b-exec-runner --version"); - assert!(version.success()); -} - -/// `--serve-exec` requires a `--slot` in range; a missing or out-of-range slot -/// is a usage error (64), and the binary never proceeds without a slot. -#[test] -fn serve_exec_rejects_missing_or_out_of_range_slot() { - let bin = env!("CARGO_BIN_EXE_d2b-exec-runner"); - - let missing_slot = Command::new(bin) - .arg("--serve-exec") - .status() - .expect("run d2b-exec-runner --serve-exec"); - assert_eq!(missing_slot.code(), Some(64)); - - let out_of_range = Command::new(bin) - .args(["--serve-exec", "--slot", "99"]) - .status() - .expect("run d2b-exec-runner --serve-exec --slot 99"); - assert_eq!(out_of_range.code(), Some(64)); - - let non_numeric = Command::new(bin) - .args(["--serve-exec", "--slot", "xx"]) - .status() - .expect("run d2b-exec-runner --serve-exec --slot xx"); - assert_eq!(non_numeric.code(), Some(64)); -} diff --git a/packages/d2b-exec-runner/tests/tty_pty_integration.rs b/packages/d2b-exec-runner/tests/tty_pty_integration.rs deleted file mode 100644 index 05e885bdb4..0000000000 --- a/packages/d2b-exec-runner/tests/tty_pty_integration.rs +++ /dev/null @@ -1,579 +0,0 @@ -//! Real-PTY integration coverage for the `d2b-exec-runner --tty-exec` -//! helper, exercised exactly the way guestd's PTY spawner drives it: allocate a -//! PTY master/slave pair, spawn the helper with the slave on stdin and a -//! `O_CLOEXEC` status pipe on stdout, and assert the controlling-terminal -//! handshake. -//! -//! These assertions are the real-kernel half of the W14 coverage; the guestd -//! runtime state machine (offset machine, control-seq dispatcher, teardown -//! phases) is covered by the fake-driven matrix in `d2b-guestd`. -//! -//! Linux-only: the helper relies on `setsid`/`TIOCSCTTY`/PTY semantics. -#![cfg(target_os = "linux")] - -use std::os::fd::OwnedFd; -use std::process::{Command, Stdio}; -use std::thread::sleep; -use std::time::{Duration, Instant}; - -use rustix::fs::{Mode, OFlags, open}; -use rustix::io::{ioctl_fionbio, read}; -use rustix::pipe::{PipeFlags, pipe_with}; -use rustix::process::{Pid, Signal, kill_process, kill_process_group, test_kill_process}; -use rustix::pty::{OpenptFlags, grantpt, openpt, ptsname, unlockpt}; -use rustix::termios::{Winsize, tcgetpgrp, tcgetsid, tcsetwinsize}; - -/// Generous wall-clock budget for the signal-/PTY-delivery polls below. The -/// polls break as soon as their condition is met (~1s on an idle machine), so a -/// large ceiling does not slow the happy path - it only keeps the suite green -/// under heavy CI load (e.g. when the rust workspace gate runs concurrently -/// with the Nix eval region). A tight 5s ceiling was racy under that load. -const WAIT: Duration = Duration::from_secs(30); - -/// Hard ceiling for the SIGHUP-on-hangup watchdog. The session leader is -/// reaped with a blocking `wait()`, so this is NOT a normal-path budget - it -/// only bounds the pathological case where the leader genuinely ignores SIGHUP -/// (a real bug), escalating to SIGKILL so the test fails loudly instead of -/// hanging. Generous so heavy co-scheduled load never trips it spuriously. -const WATCHDOG: Duration = Duration::from_secs(120); - -/// Allocate a PTY master/slave pair the way the spawner does: master AND slave -/// both `O_RDWR|O_NOCTTY|O_CLOEXEC`. The slave is `O_CLOEXEC` (matching the G4 -/// production contract) so a concurrent fork/exec elsewhere cannot inherit it -/// and keep the PTY alive; `Stdio::from(slave)` still hands fd0 to the helper -/// because `Command`'s dup2 clears `CLOEXEC` on the duplicate. -fn open_pty() -> (OwnedFd, OwnedFd) { - let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC) - .expect("openpt master"); - grantpt(&master).expect("grantpt"); - unlockpt(&master).expect("unlockpt"); - let slave_path = ptsname(&master, Vec::new()).expect("ptsname"); - let slave = open( - &slave_path, - OFlags::RDWR | OFlags::NOCTTY | OFlags::CLOEXEC, - Mode::empty(), - ) - .expect("open slave"); - (master, slave) -} - -/// Drain the master into a persistent accumulator until `needle` is observed or -/// the deadline elapses. Returns whether the needle is present. Using a single -/// growing buffer across phases avoids losing bytes that arrive batched with an -/// earlier needle. -fn drain_until(master: &OwnedFd, acc: &mut String, needle: &str, deadline: Duration) -> bool { - if acc.contains(needle) { - return true; - } - let start = Instant::now(); - let mut buf = [0u8; 4096]; - while start.elapsed() < deadline { - match read(master, &mut buf) { - Ok(0) => return acc.contains(needle), // EOF: all slaves closed. - Ok(n) => { - acc.push_str(&String::from_utf8_lossy(&buf[..n])); - if acc.contains(needle) { - return true; - } - } - Err(err) if err == rustix::io::Errno::AGAIN => sleep(Duration::from_millis(10)), - Err(err) if err == rustix::io::Errno::IO => return acc.contains(needle), - Err(_) => return acc.contains(needle), - } - } - acc.contains(needle) -} - -/// Poll `tcgetsid(master)` until it resolves (the helper has acquired the slave -/// as its controlling terminal) or the deadline elapses. -fn wait_for_ctty_sid(master: &OwnedFd, deadline: Duration) -> Option { - let start = Instant::now(); - while start.elapsed() < deadline { - if let Ok(pid) = tcgetsid(master) { - return Some(pid.as_raw_nonzero().get()); - } - sleep(Duration::from_millis(10)); - } - None -} - -#[test] -fn tty_helper_establishes_session_ctty_winsize_winch_and_hangup() { - let bin = env!("CARGO_BIN_EXE_d2b-exec-runner"); - let (master, slave) = open_pty(); - // CLOEXEC status pipe: the write end closes on a successful exec (EOF), and - // the helper never leaks it into the target (proves the CLOEXEC handoff). - let (status_r, status_w) = pipe_with(PipeFlags::CLOEXEC).expect("status pipe"); - - // The target prints its initial winsize, then re-prints it on every - // SIGWINCH, and backgrounds an in-session child that IGNORES SIGHUP (so it - // survives the terminal hangup the foreground shell dies from). It stays in - // the helper-created session, so only the sid-scoped reap - not the SIGHUP - // and not a foreground-PG kill - can clean it up. A `sleep` loop (rather - // than a blocking `read`) keeps the shell responsive so a trapped SIGWINCH - // runs promptly between commands, and a SIGHUP on hangup still terminates it. - // - // Some long-running gate launchers enter `cargo test` with SIGHUP inherited - // as SIG_IGN. Ignored dispositions survive exec, and `/bin/sh` cannot - // reliably reset an ignored-on-entry SIGHUP itself, so normalize HUP with - // `env --default-signal=HUP` before execing the shell. `env` execs in-place, - // preserving the helper's PID/SID while making the leader's SIGHUP contract - // explicit and deterministic. - let script = "stty size; \ - trap 'stty size' WINCH; \ - ( trap '' HUP; exec sleep 600 ) & \ - echo \"BG:$!\"; \ - echo READY; \ - while :; do sleep 1; done"; - - let mut child = Command::new(bin) - .args([ - "--tty-exec", - "--rows", - "30", - "--cols", - "100", - "--", - "/usr/bin/env", - "--default-signal=HUP", - "/bin/sh", - "-c", - script, - ]) - // Safe fd handoff, mirroring the production spawner: slave on stdin, - // status pipe on stdout, stderr to null. No pass_fds / pre_exec. - .stdin(Stdio::from(slave)) - .stdout(Stdio::from(status_w)) - .stderr(Stdio::null()) - .spawn() - .expect("spawn --tty-exec helper"); - let child_pid = child.id() as i32; - - // The master is non-blocking so reads never wedge the test. - ioctl_fionbio(&master, true).expect("master O_NONBLOCK"); - - // 1. Success handshake: the status pipe reaches EOF (no failure byte) once - // the helper exec's the target. A non-empty read would be a typed - // failure byte (setup/exec failure). - { - let status = status_r; - ioctl_fionbio(&status, true).expect("status O_NONBLOCK"); - let start = Instant::now(); - let mut handshake: Option> = None; - let mut byte = [0u8; 1]; - while start.elapsed() < WAIT { - match read(&status, &mut byte) { - Ok(n) => { - handshake = Some(Ok(n)); - break; - } - Err(err) if err == rustix::io::Errno::AGAIN => sleep(Duration::from_millis(10)), - Err(err) => { - handshake = Some(Err(err.into())); - break; - } - } - } - match handshake { - Some(Ok(0)) => {} // EOF == exec succeeded. - other => panic!("expected status-pipe EOF on success, got {other:?}"), - } - // `status` drops here, closing the status reader. - } - - // 2. Session leader + controlling terminal: tcgetsid(master) resolves to the - // child's pid (setsid made pid == sid; TIOCSCTTY bound the slave). - let sid = wait_for_ctty_sid(&master, WAIT).expect("controlling-terminal session id"); - assert_eq!( - sid, child_pid, - "session leader pid must equal the spawned helper/target pid" - ); - - // 3. Initial winsize applied by the helper's tcsetwinsize, and the - // in-session background child's pid: the script prints `stty size` - // ("rows cols"), then "BG:", then "READY". A single accumulator - // keeps every byte regardless of how reads batch. - let mut transcript = String::new(); - assert!( - drain_until(&master, &mut transcript, "READY", WAIT), - "expected READY from target, saw: {transcript:?}" - ); - assert!( - transcript.contains("30 100"), - "expected initial winsize 30 100, saw: {transcript:?}" - ); - let bg_pid: i32 = transcript - .lines() - .find_map(|line| line.trim().strip_prefix("BG:")) - .and_then(|pid| pid.trim().parse().ok()) - .unwrap_or_else(|| panic!("expected BG: line, saw: {transcript:?}")); - - // 4. SIGWINCH: resizing the master delivers SIGWINCH to the foreground - // process group; the trap re-runs `stty size` with the new geometry. - tcsetwinsize( - &master, - Winsize { - ws_row: 40, - ws_col: 120, - ws_xpixel: 0, - ws_ypixel: 0, - }, - ) - .expect("resize master"); - assert!( - drain_until(&master, &mut transcript, "40 120", WAIT), - "expected post-SIGWINCH winsize 40 120, saw: {transcript:?}" - ); - - // 5. SIGHUP on hangup: dropping the master (the last master reference) closes - // the terminal, so the kernel sends SIGHUP to the session leader, which - // terminates. Send an explicit SIGHUP to the leader as well (the kernel - // terminal-hangup delivery can race). Then poll for the leader to exit - // with NO fixed budget for the success path: under heavy co-scheduled - // load (the rust gate running concurrently with the Nix eval region) the - // leader can be scheduler-starved for tens of seconds, and a wall-clock - // deadline would flake. Only after a generous WATCHDOG ceiling - which a - // healthy leader never reaches - do we treat a still-alive leader as a - // genuine SIGHUP-ignored bug, SIGKILL it to avoid hanging the suite, and - // fail the assertion. SIGHUP goes to the leader's PID only (not the - // process group), so the HUP-trapping in-session background child checked - // in step 6 still survives. - drop(master); - if let Some(pid) = Pid::from_raw(child_pid) { - let _ = kill_process(pid, Signal::Hup); - } - let start = Instant::now(); - let mut hup_ignored = false; - loop { - match child.try_wait() { - Ok(Some(_status)) => break, - Ok(None) => { - if start.elapsed() > WATCHDOG { - if let Some(pid) = Pid::from_raw(child_pid) { - let _ = kill_process(pid, Signal::Kill); - } - hup_ignored = true; - break; - } - sleep(Duration::from_millis(20)); - } - Err(_) => break, - } - } - // Reap the leader (the SIGKILL path leaves a just-killed child to collect). - let _ = child.wait(); - assert!( - !hup_ignored, - "session leader did not exit after master hangup (SIGHUP ignored)" - ); - - // 6. In-session no-orphan: the background child shares the helper-created - // session (sid == child_pid) and is NOT a direct child of the helper's - // foreground process, so SIGHUP to the session leader does not reach it - - // it must still be ALIVE here, BEFORE guestd's reaper runs. This is the - // exact condition ProcSessionReaper exists to handle. - assert!( - Pid::from_raw(bg_pid) - .map(|pid| test_kill_process(pid).is_ok()) - .unwrap_or(false), - "in-session background child {bg_pid} should still be alive after SIGHUP, before the sid-scoped reap" - ); - // Reap by the SAME sid-scoped logic guestd uses (ProcSessionReaper): scan - // /proc for every pid whose session id == child_pid and SIGKILL it - not by - // killing child_pid's process group. This proves the foundation of guestd's - // no-orphan teardown. A setsid/double-fork escapee would be out of scope - // (documented trusted-root limitation). - for _ in 0..50 { - let pids = pids_in_session(child_pid); - if pids.is_empty() { - break; - } - for pid in pids { - if let Some(pid) = Pid::from_raw(pid) { - let _ = kill_process(pid, Signal::Kill); - } - } - sleep(Duration::from_millis(10)); - } - let start = Instant::now(); - let mut reaped = false; - while start.elapsed() < WAIT { - // test_kill_process is kill(pid, 0): Err(ESRCH) once the pid is gone. - if let Some(pid) = Pid::from_raw(bg_pid) - && test_kill_process(pid).is_err() - { - reaped = true; - break; - } - sleep(Duration::from_millis(10)); - } - assert!( - reaped, - "in-session background child {bg_pid} was not reaped by the sid-scoped sweep" - ); -} - -/// Parse a labelled decimal pid (`