diff --git a/.github/workflows/ci-fast.yml b/.github/workflows/ci-fast.yml index 0cd515dd8..d069497af 100644 --- a/.github/workflows/ci-fast.yml +++ b/.github/workflows/ci-fast.yml @@ -155,13 +155,14 @@ jobs: - name: TypeScript unit tests # The per-package vitest suites (commands receiver twins, bindings - # loader/error mapping, core schemas) — `test:ts` above is - # typecheck-only, so without this step they never ran in CI. Must run - # after the dev-addon build: the bindings kv/queue/storage/vault - # suites load the native addon. + # loader/error mapping, core schemas, ai-gateway client/gateway) — + # `test:ts` above is typecheck-only, so without this step they never + # ran in CI. Must run after the dev-addon build: the bindings + # kv/queue/storage/vault suites load the native addon. The ai-gateway + # suites inject a mock addon, so they need no built .node here. env: NODE_OPTIONS: "--max-old-space-size=8192" - run: pnpm --filter @alienplatform/core --filter @alienplatform/bindings --filter @alienplatform/commands test + run: pnpm --filter @alienplatform/core --filter @alienplatform/bindings --filter @alienplatform/commands --filter @alienplatform/ai-gateway test - name: Rust fast tests (non-cloud) env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fb32e198..356049085 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,10 @@ jobs: client-sdks/platform/typescript client-sdks/manager/typescript \ packages/bindings crates/alien-bindings-node \ packages/bindings/npm/darwin-arm64 packages/bindings/npm/darwin-x64 \ - packages/bindings/npm/linux-x64-gnu packages/bindings/npm/linux-arm64-gnu; do + packages/bindings/npm/linux-x64-gnu packages/bindings/npm/linux-arm64-gnu \ + packages/ai-gateway crates/alien-ai-gateway-node \ + packages/ai-gateway/npm/darwin-arm64 packages/ai-gateway/npm/darwin-x64 \ + packages/ai-gateway/npm/linux-x64-gnu packages/ai-gateway/npm/linux-arm64-gnu; do node -e " const fs = require('fs'); const path = '${pkg}/package.json'; @@ -151,7 +154,10 @@ jobs: client-sdks/platform/typescript/package.json client-sdks/manager/typescript/package.json \ packages/bindings/package.json crates/alien-bindings-node/package.json \ packages/bindings/npm/darwin-arm64/package.json packages/bindings/npm/darwin-x64/package.json \ - packages/bindings/npm/linux-x64-gnu/package.json packages/bindings/npm/linux-arm64-gnu/package.json + packages/bindings/npm/linux-x64-gnu/package.json packages/bindings/npm/linux-arm64-gnu/package.json \ + packages/ai-gateway/package.json crates/alien-ai-gateway-node/package.json \ + packages/ai-gateway/npm/darwin-arm64/package.json packages/ai-gateway/npm/darwin-x64/package.json \ + packages/ai-gateway/npm/linux-x64-gnu/package.json packages/ai-gateway/npm/linux-arm64-gnu/package.json git commit -m "chore: release v${VERSION}" git tag "v${VERSION}" git push @@ -397,6 +403,11 @@ jobs: VERSION: ${{ needs.prepare.outputs.version }} run: node packages/bindings/scripts/validate-release-version.mjs "$VERSION" + - name: Validate ai-gateway release version lockstep + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: node packages/ai-gateway/scripts/validate-release-version.mjs "$VERSION" + # Rust toolchain: Linux via the shared composite; macOS uses dtolnay + # brew protoc directly (plain `napi build`, not `depot cargo`, so no # Depot/sccache setup here). The addon depends on alien-bindings, whose @@ -460,6 +471,31 @@ jobs: retention-days: 1 path: packages/bindings/npm/${{ matrix.triple }}/alien-bindings-node.${{ matrix.triple }}.node + # The AI gateway addon reuses this matrix (same runners/toolchain) — it also + # depends on alien-bindings' credential resolver, so protoc is already set up. + - name: Build ai-gateway addon (${{ matrix.triple }}) + env: + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + run: | + pnpm --filter @alienplatform/ai-gateway exec napi build \ + --platform --release --target ${{ matrix.target }} \ + --cwd "$GITHUB_WORKSPACE/crates/alien-ai-gateway-node" + + - name: Stage ai-gateway addon into platform package + run: | + cp "crates/alien-ai-gateway-node/alien-ai-gateway-node.${{ matrix.triple }}.node" \ + "packages/ai-gateway/npm/${{ matrix.triple }}/alien-ai-gateway-node.${{ matrix.triple }}.node" + + - name: Assert staged ai-gateway binary architecture + run: file "packages/ai-gateway/npm/${{ matrix.triple }}/alien-ai-gateway-node.${{ matrix.triple }}.node" | grep -q "${{ matrix.expect_arch }}" + + - name: Upload ai-gateway addon artifact + uses: actions/upload-artifact@v4 + with: + name: ai-gateway-addon-${{ matrix.triple }} + retention-days: 1 + path: packages/ai-gateway/npm/${{ matrix.triple }}/alien-ai-gateway-node.${{ matrix.triple }}.node + # This job deliberately has no Rust compiler or protoc setup. It # starts from the already-built artifacts and proves that a consumer whose # only direct dependency is @alienplatform/bindings gets the right optional @@ -503,6 +539,11 @@ jobs: VERSION: ${{ needs.prepare.outputs.version }} run: node packages/bindings/scripts/validate-release-version.mjs "$VERSION" + - name: Validate ai-gateway release version lockstep + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: node packages/ai-gateway/scripts/validate-release-version.mjs "$VERSION" + - uses: pnpm/action-setup@v6 with: version: 10.11.0 @@ -561,6 +602,11 @@ jobs: VERSION: ${{ needs.prepare.outputs.version }} run: node packages/bindings/scripts/validate-release-version.mjs "$VERSION" + - name: Validate ai-gateway release version lockstep + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: node packages/ai-gateway/scripts/validate-release-version.mjs "$VERSION" + - uses: pnpm/action-setup@v6 with: version: 10.11.0 @@ -645,6 +691,76 @@ jobs: pnpm --filter @alienplatform/bindings publish --access public --no-git-checks fi + publish-ai-gateway: + needs: [prepare, build-addon, publish-npm] + # Same ordering rules as publish-bindings: the wrapper pins + # `@alienplatform/core: ^`, so it publishes only after core, and + # its per-platform prebuilds publish before the wrapper that pins them. + if: always() && needs.build-addon.result == 'success' && (inputs.dry_run || needs.publish-npm.result == 'success') + runs-on: depot-ubuntu-24.04-arm-4 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.dry_run && github.sha || format('v{0}', needs.prepare.outputs.version) }} + + - uses: pnpm/action-setup@v4 + with: + version: 10.11.0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + registry-url: https://registry.npmjs.org + + - name: Validate npm auth + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm whoami + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Download ai-gateway addon artifacts + uses: actions/download-artifact@v4 + with: + pattern: ai-gateway-addon-* + path: ./addons + merge-multiple: false + + - name: Stage addons into platform packages + run: | + for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu; do + cp "./addons/ai-gateway-addon-${triple}/alien-ai-gateway-node.${triple}.node" \ + "packages/ai-gateway/npm/${triple}/alien-ai-gateway-node.${triple}.node" + done + + - name: Build wrapper + run: pnpm --filter @alienplatform/ai-gateway build + + - name: Publish platform prebuild packages (first) + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + DRY="" + if [ "${{ inputs.dry_run }}" = "true" ]; then DRY="--dry-run"; fi + for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu; do + echo "Publishing @alienplatform/ai-gateway-${triple}..." + ( cd "packages/ai-gateway/npm/${triple}" && npm publish --access public $DRY ) + done + + - name: Inject optionalDependencies into the wrapper + run: node packages/ai-gateway/scripts/inject-optional-deps.mjs + + - name: Publish wrapper (last) + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + DRY="" + if [ "${{ inputs.dry_run }}" = "true" ]; then DRY="--dry-run"; fi + pnpm --filter @alienplatform/ai-gateway publish --access public --no-git-checks $DRY + # ─── Binary builds (4 targets, all parallel) ─────────────────────── build-binaries-linux-x86_64: diff --git a/Cargo.lock b/Cargo.lock index 823832b2a..85f1c3743 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,9 +83,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "alien-ai-gateway-node" +version = "2.1.6" +dependencies = [ + "alien-error", + "alien-gateway", + "napi", + "napi-build", + "napi-derive", + "serde", + "serde_json", +] + [[package]] name = "alien-aws-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-client-core", @@ -127,7 +140,7 @@ dependencies = [ [[package]] name = "alien-azure-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-client-core", "alien-core", @@ -171,7 +184,7 @@ dependencies = [ [[package]] name = "alien-bindings" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -215,7 +228,7 @@ dependencies = [ [[package]] name = "alien-bindings-node" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-error", @@ -232,7 +245,7 @@ dependencies = [ [[package]] name = "alien-build" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-build", "alien-core", @@ -266,7 +279,7 @@ dependencies = [ [[package]] name = "alien-cli" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-build", @@ -337,7 +350,7 @@ dependencies = [ [[package]] name = "alien-cli-common" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-deployment", @@ -351,7 +364,7 @@ dependencies = [ [[package]] name = "alien-client-config" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -369,7 +382,7 @@ dependencies = [ [[package]] name = "alien-client-core" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "anyhow", @@ -388,7 +401,7 @@ dependencies = [ [[package]] name = "alien-cloudformation" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -403,7 +416,7 @@ dependencies = [ [[package]] name = "alien-commands" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -441,7 +454,7 @@ dependencies = [ [[package]] name = "alien-commands-client" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "base64 0.22.1", @@ -456,7 +469,7 @@ dependencies = [ [[package]] name = "alien-core" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "alien-macros", @@ -488,7 +501,7 @@ dependencies = [ [[package]] name = "alien-deploy-cli" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-cli-common", "alien-core", @@ -531,7 +544,7 @@ dependencies = [ [[package]] name = "alien-deployment" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -562,7 +575,7 @@ dependencies = [ [[package]] name = "alien-error" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error-derive", "anyhow", @@ -575,7 +588,7 @@ dependencies = [ [[package]] name = "alien-error-derive" -version = "2.0.2" +version = "2.1.6" dependencies = [ "proc-macro2", "quote", @@ -583,9 +596,36 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "alien-gateway" +version = "2.1.6" +dependencies = [ + "alien-bindings", + "alien-core", + "alien-error", + "async-trait", + "aws-config", + "aws-credential-types", + "aws-sigv4", + "aws-smithy-eventstream", + "aws-smithy-types", + "axum 0.8.9", + "base64 0.22.1", + "futures", + "http 1.4.2", + "httpmock", + "reqwest 0.12.28", + "serde", + "serde_json", + "temp-env", + "tokio", + "tracing", + "urlencoding", +] + [[package]] name = "alien-gcp-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-client-core", "alien-core", @@ -618,7 +658,7 @@ dependencies = [ [[package]] name = "alien-helm" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -632,7 +672,7 @@ dependencies = [ [[package]] name = "alien-infra" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -692,7 +732,7 @@ dependencies = [ [[package]] name = "alien-k8s-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-client-core", "alien-core", @@ -721,7 +761,7 @@ dependencies = [ [[package]] name = "alien-local" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-build", @@ -764,7 +804,7 @@ dependencies = [ [[package]] name = "alien-macros" -version = "2.0.2" +version = "2.1.6" dependencies = [ "proc-macro2", "quote", @@ -773,7 +813,7 @@ dependencies = [ [[package]] name = "alien-manager" -version = "2.0.2" +version = "2.1.6" dependencies = [ "aegis", "alien-azure-clients", @@ -834,7 +874,7 @@ dependencies = [ [[package]] name = "alien-manager-api" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "chrono", @@ -853,7 +893,7 @@ dependencies = [ [[package]] name = "alien-observer" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -873,7 +913,7 @@ dependencies = [ [[package]] name = "alien-operator" -version = "2.0.2" +version = "2.1.6" dependencies = [ "aegis", "alien-client-config", @@ -917,7 +957,7 @@ dependencies = [ [[package]] name = "alien-permissions" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -936,7 +976,7 @@ dependencies = [ [[package]] name = "alien-platform-api" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "chrono", @@ -955,7 +995,7 @@ dependencies = [ [[package]] name = "alien-preflights" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -972,7 +1012,7 @@ dependencies = [ [[package]] name = "alien-sdk" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-core", @@ -994,7 +1034,7 @@ dependencies = [ [[package]] name = "alien-terraform" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -1009,7 +1049,7 @@ dependencies = [ [[package]] name = "alien-test" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -1056,7 +1096,7 @@ dependencies = [ [[package]] name = "alien-test-app" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "alien-sdk", @@ -1102,7 +1142,7 @@ dependencies = [ [[package]] name = "alien-worker-protocol" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "async-stream", @@ -1121,7 +1161,7 @@ dependencies = [ [[package]] name = "alien-worker-runtime" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-commands", @@ -1831,6 +1871,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d8391e65fcea47c586a22e1a41f173b38615b112b2c6b7a44e80cec3e6b706" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + [[package]] name = "aws-smithy-http" version = "0.63.6" diff --git a/Cargo.toml b/Cargo.toml index ba71eca1e..43fde8cda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ members = [ "crates/alien-k8s-clients", "crates/alien-observer", "crates/alien-error", + "crates/alien-gateway", + "crates/alien-ai-gateway-node", "crates/alien-error-derive", "crates/alien-permissions", "crates/alien-preflights", @@ -48,6 +50,8 @@ resolver = "2" default-members = [ "crates/alien-core", "crates/alien-error", + "crates/alien-gateway", + "crates/alien-ai-gateway-node", "crates/alien-macros" ] @@ -64,6 +68,7 @@ license-file = "LICENSE.md" alien-commands = { path = "crates/alien-commands", version = "2.1.6", default-features = false } alien-commands-client = { path = "crates/alien-commands-client" } alien-bindings = { path = "crates/alien-bindings", version = "2.1.6", default-features = false } +alien-gateway = { path = "crates/alien-gateway", version = "2.1.6" } alien-worker-protocol = { path = "crates/alien-worker-protocol", version = "2.1.6" } alien-sdk = { path = "crates/alien-sdk", version = "2.1.6", default-features = false } alien-worker-runtime = { path = "crates/alien-worker-runtime", version = "2.1.6" } @@ -98,6 +103,7 @@ alien-platform-api = { path = "client-sdks/platform/rust", version = "2.1.6", de anyhow = "1.0" async-stream = "0.3" async-trait = "0.1.88" +aws-config = "1.5" aws-credential-types = "1.2" aws_lambda_events = { version = "0.16", features = ["cloudwatch_events", "s3", "sqs"] } aws-sigv4 = "1.3.2" @@ -209,6 +215,7 @@ wasm-bindgen-futures = "0.4.40" wasm-bindgen-test = "0.3.0" console_error_panic_hook = "0.1.7" aws-smithy-async = "1.2.5" +aws-smithy-eventstream = "0.60" aws-smithy-runtime-api = "1.8" aws-smithy-types = "1.3" getrandom = { version = "0.3", features = ["wasm_js"] } diff --git a/crates/alien-ai-gateway-node/.gitignore b/crates/alien-ai-gateway-node/.gitignore new file mode 100644 index 000000000..2f6761404 --- /dev/null +++ b/crates/alien-ai-gateway-node/.gitignore @@ -0,0 +1,10 @@ +# Prebuilt native binaries — produced by `napi build`, assembled into per-platform +# npm packages at build time; never committed. +*.node +node_modules/ +# napi-rs generates these beside the native translation crate, but the public +# loader and declarations live in packages/ai-gateway. Keep the generated shim +# out of the repository so it cannot become a second public surface. +index.js +index.d.ts +pnpm-lock.yaml diff --git a/crates/alien-ai-gateway-node/Cargo.toml b/crates/alien-ai-gateway-node/Cargo.toml new file mode 100644 index 000000000..8e30c33b1 --- /dev/null +++ b/crates/alien-ai-gateway-node/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "alien-ai-gateway-node" +version.workspace = true +edition = "2021" +description = "Node-API (napi-rs) addon exposing the alien AI gateway to JavaScript" +license-file.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +alien-gateway = { workspace = true } +alien-error = { workspace = true } +napi = { workspace = true } +napi-derive = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } + +[build-dependencies] +napi-build = { workspace = true } diff --git a/crates/alien-ai-gateway-node/build.rs b/crates/alien-ai-gateway-node/build.rs new file mode 100644 index 000000000..e8ebbfcb6 --- /dev/null +++ b/crates/alien-ai-gateway-node/build.rs @@ -0,0 +1,18 @@ +fn main() { + napi_build::setup(); + + // `cargo test` links a plain executable with no Node/Bun host, so the `napi_*` + // symbols the cdylib resolves at load time are undefined at link time. Defer + // resolution for every locally-linked artifact — the mapper tests never call + // into napi. + match std::env::var("CARGO_CFG_TARGET_OS").as_deref() { + Ok("macos") => { + println!("cargo:rustc-link-arg=-undefined"); + println!("cargo:rustc-link-arg=dynamic_lookup"); + } + Ok("linux") => { + println!("cargo:rustc-link-arg=-Wl,--unresolved-symbols=ignore-all"); + } + _ => {} + } +} diff --git a/crates/alien-ai-gateway-node/package.json b/crates/alien-ai-gateway-node/package.json new file mode 100644 index 000000000..8ef6895a7 --- /dev/null +++ b/crates/alien-ai-gateway-node/package.json @@ -0,0 +1,19 @@ +{ + "name": "@alienplatform/ai-gateway-node", + "version": "0.0.0", + "private": true, + "description": "napi-rs addon exposing the alien AI gateway to JavaScript", + "napi": { + "binaryName": "alien-ai-gateway-node", + "packageName": "@alienplatform/ai-gateway", + "targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu" + ] + }, + "devDependencies": { + "@napi-rs/cli": "^3.0.0" + } +} diff --git a/crates/alien-ai-gateway-node/src/error.rs b/crates/alien-ai-gateway-node/src/error.rs new file mode 100644 index 000000000..a767ef270 --- /dev/null +++ b/crates/alien-ai-gateway-node/src/error.rs @@ -0,0 +1,47 @@ +//! Error translation from `alien-gateway` into `napi::Error`. +//! +//! Mirrors the envelope contract used by `alien-bindings-node`: async `#[napi]` +//! methods can only carry data to JS through the error `reason` (napi's `Status` +//! is a closed enum), so a stable JSON envelope `{ code, message, context?, +//! retryable, internal }` is serialized into `reason` and the TS layer recovers +//! it with a single `JSON.parse`. `internal` crosses the boundary so the JS side +//! can honor the Rust error's redaction posture rather than defaulting it open. + +use alien_error::AlienError; +use alien_gateway::ErrorData; +use serde::Serialize; + +#[derive(Debug, Serialize)] +struct ErrorEnvelope<'a> { + code: &'a str, + message: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + context: Option<&'a serde_json::Value>, + retryable: bool, + internal: bool, + // Carry the status code and hint too: a startup failure like + // `BindingConfigInvalid` declares `http_status_code = 400`, and without this + // the TS side would default it to 500 — a user-fixable config error rendered + // as a server fault. + #[serde(rename = "httpStatusCode", skip_serializing_if = "Option::is_none")] + http_status_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + hint: Option<&'a str>, +} + +/// Serialize an `AlienError` into a `napi::Error` carrying the structured envelope +/// in its `reason`. The napi `Status` is always `GenericFailure`; the real code +/// lives in the envelope. +pub fn map_gateway_error(err: AlienError) -> napi::Error { + let envelope = ErrorEnvelope { + code: &err.code, + message: &err.message, + context: err.context.as_ref(), + retryable: err.retryable, + internal: err.internal, + http_status_code: err.http_status_code, + hint: err.hint.as_deref(), + }; + let reason = serde_json::to_string(&envelope).unwrap_or_else(|_| err.message.clone()); + napi::Error::new(napi::Status::GenericFailure, reason) +} diff --git a/crates/alien-ai-gateway-node/src/lib.rs b/crates/alien-ai-gateway-node/src/lib.rs new file mode 100644 index 000000000..f10378057 --- /dev/null +++ b/crates/alien-ai-gateway-node/src/lib.rs @@ -0,0 +1,52 @@ +//! Node-API addon for the alien AI gateway. +//! +//! The gateway itself is the Rust `alien-gateway` crate — the single, shared +//! implementation. This addon is a thin wrapper: it starts the gateway's loopback +//! HTTP server inside napi's tokio runtime and returns the server's URL to JS. +//! The application then points a plain OpenAI-compatible client at that URL, and +//! every request/response (including SSE token streams) flows over the loopback +//! HTTP socket — never across the napi boundary. + +use alien_gateway::{bindings_from_env, start_gateway, GatewayHandle}; +use napi_derive::napi; + +mod error; +use error::map_gateway_error; + +#[napi] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} + +/// A running gateway: its loopback base URL and the underlying handle, which owns +/// the server task. JS must hold this object for as long as it uses the gateway — +/// dropping it aborts the server. +#[napi] +pub struct AiGatewayHandle { + url: String, + _handle: GatewayHandle, +} + +#[napi] +impl AiGatewayHandle { + /// The gateway's loopback base URL (`http://127.0.0.1:`). Callers append + /// `//v1` and point an OpenAI-compatible client at it. + #[napi(getter)] + pub fn url(&self) -> String { + self.url.clone() + } +} + +/// Start the embedded AI gateway from the ambient `ALIEN__BINDING` env vars +/// in the current process, and return a handle whose `url` is the loopback base. +/// A no-op-cost call when the process links no ambient AI resource (the gateway +/// serves no routes). Hold the returned handle for the process lifetime. +#[napi] +pub async fn start_ai_gateway() -> napi::Result { + let bindings = bindings_from_env().map_err(map_gateway_error)?; + let handle = start_gateway(bindings).await.map_err(map_gateway_error)?; + Ok(AiGatewayHandle { + url: handle.url.clone(), + _handle: handle, + }) +} diff --git a/crates/alien-aws-clients/src/aws/bedrock.rs b/crates/alien-aws-clients/src/aws/bedrock.rs new file mode 100644 index 000000000..326f7dc49 --- /dev/null +++ b/crates/alien-aws-clients/src/aws/bedrock.rs @@ -0,0 +1,661 @@ +//! AWS Bedrock control-plane client (model customization / fine-tuning). +//! +//! Hand-rolled reqwest + SigV4 client for the two model-customization-job REST +//! operations the fine-tuning controller needs. There is deliberately no +//! `aws-sdk-bedrock` dependency; this mirrors the other reqwest-based clients in +//! this crate (see `s3.rs` / `apigatewayv2.rs`) and keeps the dependency surface +//! small. +//! +//! Bedrock control plane host: `https://bedrock.{region}.amazonaws.com` +//! (SigV4 service signing name `bedrock`). +//! - CreateModelCustomizationJob: `POST /model-customization-jobs` +//! - GetModelCustomizationJob: `GET /model-customization-jobs/{jobIdentifier}` +//! +//! Request/response JSON field names follow the AWS Bedrock API reference +//! (CreateModelCustomizationJob / GetModelCustomizationJob, service version +//! 2023-04-20). Only the fields the controller uses are modeled; everything else +//! is ignored on deserialize. + +use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsSignConfig}; +use crate::aws::credential_provider::AwsCredentialProvider; +use alien_client_core::{ErrorData, Result}; +use alien_error::{Context, ContextError, IntoAlienError}; +use async_trait::async_trait; +use bon::Builder; +use form_urlencoded; +use reqwest::{Client, Method, StatusCode}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[cfg(feature = "test-utils")] +use mockall::automock; + +// --------------------------------------------------------------------------- +// Bedrock API trait +// --------------------------------------------------------------------------- + +/// Minimal Bedrock control-plane surface for model customization (fine-tuning). +#[cfg_attr(feature = "test-utils", automock)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait BedrockApi: Send + Sync + std::fmt::Debug { + /// Submit a model customization (fine-tuning) job. Returns the created job's ARN, + /// which is a valid `jobIdentifier` for [`get_model_customization_job`]. + async fn create_model_customization_job( + &self, + request: &CreateModelCustomizationJobRequest, + ) -> Result; + + /// Fetch a model customization job's current status and (once complete) the + /// resulting custom-model ARN. `job_identifier` is the job ARN or job name. + async fn get_model_customization_job( + &self, + job_identifier: &str, + ) -> Result; +} + +// --------------------------------------------------------------------------- +// Client +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub struct BedrockClient { + client: Client, + credentials: AwsCredentialProvider, +} + +impl BedrockClient { + pub fn new(client: Client, credentials: AwsCredentialProvider) -> Self { + Self { + client, + credentials, + } + } + + fn sign_config(&self) -> AwsSignConfig { + AwsSignConfig { + service_name: "bedrock".into(), + region: self.credentials.region().to_string(), + credentials: self.credentials.get_credentials(), + signing_region: None, + } + } + + /// Host header value (always the real Bedrock host so the signature matches + /// what AWS expects, even when the base URL is overridden for tests). + fn host_header(&self) -> String { + format!("bedrock.{}.amazonaws.com", self.credentials.region()) + } + + fn get_base_url(&self) -> String { + if let Some(override_url) = self.credentials.get_service_endpoint_option("bedrock") { + override_url.trim_end_matches('/').to_string() + } else { + format!("https://bedrock.{}.amazonaws.com", self.credentials.region()) + } + } + + async fn send_json( + &self, + method: Method, + path: &str, + body: Option, + operation: &str, + resource: &str, + ) -> Result { + self.credentials.ensure_fresh().await?; + let url = format!("{}{}", self.get_base_url(), path); + + let mut builder = self + .client + .request(method, &url) + .host(&self.host_header()) + .content_type_json(); + + if let Some(body) = body { + builder = builder.content_sha256(&body).body(body.clone()); + let result = + crate::aws::aws_request_utils::sign_send_json(builder, &self.sign_config()).await; + return Self::map_result(result, operation, resource, Some(&body)); + } + + builder = builder.content_sha256(""); + let result = + crate::aws::aws_request_utils::sign_send_json(builder, &self.sign_config()).await; + Self::map_result(result, operation, resource, None) + } + + fn map_result( + result: Result, + operation: &str, + resource: &str, + request_body: Option<&str>, + ) -> Result { + match result { + Ok(v) => Ok(v), + Err(e) => { + if let Some(ErrorData::HttpResponseError { + http_status, + http_response_text: Some(ref text), + .. + }) = &e.error + { + let status = StatusCode::from_u16(*http_status) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + if let Some(mapped) = + Self::map_bedrock_error(status, text, operation, resource, request_body) + { + Err(e.context(mapped)) + } else { + Err(e) + } + } else { + Err(e) + } + } + } + } + + /// Map a Bedrock error JSON body (`{"message": "...", "__type": "..."}`) to a + /// structured client error. Bedrock control-plane errors use the standard AWS + /// JSON error shape. + fn map_bedrock_error( + status: StatusCode, + body: &str, + _operation: &str, + resource: &str, + request_body: Option<&str>, + ) -> Option { + let parsed: std::result::Result = serde_json::from_str(body); + let (code, message) = match parsed { + Ok(e) => { + let code = e + .type_field + .or(e.code) + .unwrap_or_else(|| "UnknownError".into()); + // `__type` is often `com.amazonaws...#ValidationException`; keep the tail. + let code = code.rsplit(['#', '.']).next().unwrap_or(&code).to_string(); + let message = e.message.unwrap_or_else(|| "Unknown error".into()); + (code, message) + } + Err(_) => return None, + }; + + Some(match code.as_str() { + "AccessDeniedException" => ErrorData::RemoteAccessDenied { + resource_type: "BedrockModelCustomizationJob".into(), + resource_name: resource.into(), + }, + "ResourceNotFoundException" => ErrorData::RemoteResourceNotFound { + resource_type: "BedrockModelCustomizationJob".into(), + resource_name: resource.into(), + }, + "ConflictException" => ErrorData::RemoteResourceConflict { + message, + resource_type: "BedrockModelCustomizationJob".into(), + resource_name: resource.into(), + }, + "ThrottlingException" => ErrorData::RateLimitExceeded { message }, + "ServiceQuotaExceededException" => ErrorData::QuotaExceeded { message }, + "ValidationException" => ErrorData::InvalidInput { + message, + field_name: None, + }, + _ => match status { + StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound { + resource_type: "BedrockModelCustomizationJob".into(), + resource_name: resource.into(), + }, + StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied { + resource_type: "BedrockModelCustomizationJob".into(), + resource_name: resource.into(), + }, + StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded { message }, + StatusCode::SERVICE_UNAVAILABLE + | StatusCode::BAD_GATEWAY + | StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable { message }, + _ => ErrorData::HttpResponseError { + message: format!("Bedrock operation failed: {}", message), + url: "bedrock.amazonaws.com".to_string(), + http_status: status.as_u16(), + http_response_text: Some(body.into()), + http_request_text: request_body.map(|s| s.to_string()), + }, + }, + }) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl BedrockApi for BedrockClient { + async fn create_model_customization_job( + &self, + request: &CreateModelCustomizationJobRequest, + ) -> Result { + let body = serde_json::to_string(request).into_alien_error().context( + ErrorData::SerializationError { + message: "Failed to serialize CreateModelCustomizationJobRequest".to_string(), + }, + )?; + self.send_json( + Method::POST, + "/model-customization-jobs", + Some(body), + "CreateModelCustomizationJob", + &request.job_name, + ) + .await + } + + async fn get_model_customization_job( + &self, + job_identifier: &str, + ) -> Result { + // The identifier is a job ARN or name; it is a path segment and must be + // percent-encoded (ARNs contain ':' and '/'). + let encoded = form_urlencoded::byte_serialize(job_identifier.as_bytes()).collect::(); + let path = format!("/model-customization-jobs/{}", encoded); + self.send_json( + Method::GET, + &path, + None, + "GetModelCustomizationJob", + job_identifier, + ) + .await + } +} + +// --------------------------------------------------------------------------- +// Error struct +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +struct BedrockErrorResponse { + pub message: Option, + pub code: Option, + #[serde(rename = "__type")] + pub type_field: Option, +} + +// --------------------------------------------------------------------------- +// Request / response types (subset of the Bedrock API) +// --------------------------------------------------------------------------- + +/// S3 location for a job's input or output data. +/// Matches Bedrock's `TrainingDataConfig` / `OutputDataConfig` (`s3Uri`). +#[derive(Debug, Clone, Serialize, Deserialize, Builder)] +#[serde(rename_all = "camelCase")] +pub struct S3DataConfig { + /// Fully-qualified S3 URI, e.g. `s3://bucket/key`. + pub s3_uri: String, +} + +/// Request body for CreateModelCustomizationJob. +/// Field names and casing follow the AWS Bedrock API reference; only the fields +/// the fine-tuning controller sets are modeled, and optional/None fields are +/// omitted so the wire shape stays minimal. +#[derive(Debug, Clone, Serialize, Builder)] +#[serde(rename_all = "camelCase")] +pub struct CreateModelCustomizationJobRequest { + /// A name for the fine-tuning job. + pub job_name: String, + /// A name for the resulting custom model. + pub custom_model_name: String, + /// ARN of the IAM role Bedrock assumes to read training data / write output. + pub role_arn: String, + /// Base foundation-model identifier to tune. + pub base_model_identifier: String, + /// The customization type, e.g. `FINE_TUNING`. + pub customization_type: String, + /// Where the job reads its training dataset from. + pub training_data_config: S3DataConfig, + /// Where the job writes output/metrics. + pub output_data_config: S3DataConfig, + /// Optional tuning hyperparameters (string→string map). + #[serde(skip_serializing_if = "Option::is_none")] + pub hyper_parameters: Option>, +} + +/// Response body for CreateModelCustomizationJob. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateModelCustomizationJobResponse { + /// ARN of the fine-tuning job (usable as `jobIdentifier` for polling). + pub job_arn: String, +} + +/// Terminal/non-terminal status of a model customization job, mirroring the +/// Bedrock `status` enum (`InProgress | Completed | Failed | Stopping | Stopped`). +/// An unrecognized value maps to [`Unknown`](ModelCustomizationJobStatus::Unknown) +/// so a future/unexpected status is surfaced rather than silently treated as terminal. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ModelCustomizationJobStatus { + InProgress, + Completed, + Failed, + Stopping, + Stopped, + /// Any status string Bedrock returns that we don't recognize. + Unknown(String), +} + +impl ModelCustomizationJobStatus { + fn from_wire(raw: &str) -> Self { + match raw { + "InProgress" => Self::InProgress, + "Completed" => Self::Completed, + "Failed" => Self::Failed, + "Stopping" => Self::Stopping, + "Stopped" => Self::Stopped, + other => Self::Unknown(other.to_string()), + } + } + + /// True for a status the job will not move out of (success or failure). + pub fn is_terminal(&self) -> bool { + matches!(self, Self::Completed | Self::Failed | Self::Stopped) + } +} + +/// Response body for GetModelCustomizationJob (subset). +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetModelCustomizationJobResponse { + /// Raw job status string from Bedrock. Prefer [`status`](Self::status) for the + /// typed enum. + pub status: String, + /// ARN of the resulting custom model, present once the job completes. This is + /// the artifact the OpenAI chat endpoint accepts for a custom model. + #[serde(default)] + pub output_model_arn: Option, + /// Name of the resulting custom model, present once the job completes. + #[serde(default)] + pub output_model_name: Option, + /// Human-readable reason the job failed, present when `status == Failed`. + #[serde(default)] + pub failure_message: Option, + /// ARN of the customization job itself. + #[serde(default)] + pub job_arn: Option, +} + +impl GetModelCustomizationJobResponse { + /// The job's status as a typed enum. + pub fn status(&self) -> ModelCustomizationJobStatus { + ModelCustomizationJobStatus::from_wire(&self.status) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use alien_core::{AwsClientConfig, AwsCredentials, AwsServiceOverrides}; + use std::{ + collections::HashMap, + io::{Read, Write}, + net::TcpListener, + sync::{Arc, Mutex}, + }; + + #[test] + fn create_request_serializes_expected_json_shape() { + let request = CreateModelCustomizationJobRequest::builder() + .job_name("my-ai-job".to_string()) + .custom_model_name("my-ai-model".to_string()) + .role_arn("arn:aws:iam::123456789012:role/test-my-ai".to_string()) + .base_model_identifier("amazon.nova-lite-v1:0".to_string()) + .customization_type("FINE_TUNING".to_string()) + .training_data_config( + S3DataConfig::builder() + .s3_uri("s3://test-bucket/training.jsonl".to_string()) + .build(), + ) + .output_data_config( + S3DataConfig::builder() + .s3_uri("s3://test-bucket/output/my-ai/".to_string()) + .build(), + ) + .build(); + + let value = serde_json::to_value(&request).unwrap(); + assert_eq!(value["jobName"], "my-ai-job"); + assert_eq!(value["customModelName"], "my-ai-model"); + assert_eq!(value["roleArn"], "arn:aws:iam::123456789012:role/test-my-ai"); + assert_eq!(value["baseModelIdentifier"], "amazon.nova-lite-v1:0"); + assert_eq!(value["customizationType"], "FINE_TUNING"); + assert_eq!( + value["trainingDataConfig"]["s3Uri"], + "s3://test-bucket/training.jsonl" + ); + assert_eq!( + value["outputDataConfig"]["s3Uri"], + "s3://test-bucket/output/my-ai/" + ); + // Optional hyperParameters omitted when not set. + assert!(value.get("hyperParameters").is_none()); + } + + #[test] + fn get_response_maps_status_and_output_arn() { + let body = r#"{ + "status": "Completed", + "jobArn": "arn:aws:bedrock:us-east-1:123456789012:model-customization-job/amazon.nova-lite-v1:0/abcdef012345", + "outputModelArn": "arn:aws:bedrock:us-east-1:123456789012:custom-model/amazon.nova-lite-v1:0/abcdef012345", + "outputModelName": "my-ai-model" + }"#; + let parsed: GetModelCustomizationJobResponse = serde_json::from_str(body).unwrap(); + assert_eq!(parsed.status(), ModelCustomizationJobStatus::Completed); + assert!(parsed.status().is_terminal()); + assert_eq!( + parsed.output_model_arn.as_deref(), + Some("arn:aws:bedrock:us-east-1:123456789012:custom-model/amazon.nova-lite-v1:0/abcdef012345") + ); + + let in_progress: GetModelCustomizationJobResponse = + serde_json::from_str(r#"{"status":"InProgress"}"#).unwrap(); + assert_eq!( + in_progress.status(), + ModelCustomizationJobStatus::InProgress + ); + assert!(!in_progress.status().is_terminal()); + assert!(in_progress.output_model_arn.is_none()); + } + + /// Read one full HTTP request (headers + body) off a socket, using the + /// Content-Length header to know when the body is complete. + fn read_http_request(stream: &mut std::net::TcpStream) -> (String, String) { + let mut bytes = Vec::new(); + let mut buffer = [0_u8; 4096]; + loop { + let count = stream.read(&mut buffer).expect("read request"); + if count == 0 { + break; + } + bytes.extend_from_slice(&buffer[..count]); + if let Some(end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") { + let header_end = end + 4; + let headers = String::from_utf8_lossy(&bytes[..header_end]).to_string(); + let length: usize = headers + .lines() + .find_map(|line| { + line.to_ascii_lowercase() + .strip_prefix("content-length: ") + .and_then(|v| v.trim().parse().ok()) + }) + .unwrap_or(0); + if bytes.len() >= header_end + length { + let body = + String::from_utf8_lossy(&bytes[header_end..header_end + length]).to_string(); + return (headers, body); + } + } + } + (String::from_utf8_lossy(&bytes).to_string(), String::new()) + } + + fn test_config(endpoint: String) -> AwsClientConfig { + AwsClientConfig { + account_id: "123456789012".to_string(), + region: "us-east-1".to_string(), + credentials: AwsCredentials::AccessKeys { + access_key_id: "test-access".to_string(), + secret_access_key: "test-secret".to_string(), + session_token: None, + }, + service_overrides: Some(AwsServiceOverrides { + endpoints: HashMap::from([("bedrock".to_string(), endpoint)]), + }), + } + } + + #[tokio::test] + async fn create_model_customization_job_signs_and_sends_expected_request() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let endpoint = format!("http://{}", listener.local_addr().expect("local address")); + let captured: Arc> = + Arc::new(Mutex::new((String::new(), String::new()))); + let sink = captured.clone(); + + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept request"); + let (headers, body) = read_http_request(&mut stream); + *sink.lock().expect("lock") = (headers, body); + let resp_body = r#"{"jobArn":"arn:aws:bedrock:us-east-1:123456789012:model-customization-job/amazon.nova-lite-v1:0/abcdef012345"}"#; + write!( + stream, + "HTTP/1.1 201 Created\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", + resp_body.len(), + resp_body + ) + .expect("write response"); + }); + + let client = + BedrockClient::new(Client::new(), AwsCredentialProvider::from_config_sync(test_config(endpoint))); + let request = CreateModelCustomizationJobRequest::builder() + .job_name("my-ai-job".to_string()) + .custom_model_name("my-ai-model".to_string()) + .role_arn("arn:aws:iam::123456789012:role/test-my-ai".to_string()) + .base_model_identifier("amazon.nova-lite-v1:0".to_string()) + .customization_type("FINE_TUNING".to_string()) + .training_data_config( + S3DataConfig::builder() + .s3_uri("s3://test-bucket/training.jsonl".to_string()) + .build(), + ) + .output_data_config( + S3DataConfig::builder() + .s3_uri("s3://test-bucket/output/my-ai/".to_string()) + .build(), + ) + .build(); + + let response = client + .create_model_customization_job(&request) + .await + .expect("request should succeed"); + server.join().expect("server should finish"); + + assert_eq!( + response.job_arn, + "arn:aws:bedrock:us-east-1:123456789012:model-customization-job/amazon.nova-lite-v1:0/abcdef012345" + ); + + let (headers, body) = captured.lock().expect("lock").clone(); + let request_line = headers.lines().next().unwrap_or_default(); + // Method + path. + assert!( + request_line.starts_with("POST /model-customization-jobs "), + "unexpected request line: {request_line}" + ); + // SigV4 signing headers must be present and name the bedrock service. + let lower = headers.to_ascii_lowercase(); + assert!( + lower.contains("authorization: aws4-hmac-sha256"), + "missing SigV4 Authorization header:\n{headers}" + ); + assert!( + lower.contains("credential=test-access/") + && lower.contains("/us-east-1/bedrock/aws4_request"), + "Authorization must scope the signature to us-east-1/bedrock:\n{headers}" + ); + assert!(lower.contains("x-amz-date:"), "missing x-amz-date header"); + assert!( + lower.contains("x-amz-content-sha256:"), + "missing x-amz-content-sha256 header" + ); + assert!( + lower.contains("host: bedrock.us-east-1.amazonaws.com"), + "host header must be the real bedrock host for a valid signature:\n{headers}" + ); + + // Body must carry the exact JSON the API expects. + let json: serde_json::Value = serde_json::from_str(&body).expect("body is json"); + assert_eq!(json["jobName"], "my-ai-job"); + assert_eq!(json["customModelName"], "my-ai-model"); + assert_eq!(json["baseModelIdentifier"], "amazon.nova-lite-v1:0"); + assert_eq!(json["customizationType"], "FINE_TUNING"); + assert_eq!( + json["trainingDataConfig"]["s3Uri"], + "s3://test-bucket/training.jsonl" + ); + assert_eq!(json["roleArn"], "arn:aws:iam::123456789012:role/test-my-ai"); + } + + #[tokio::test] + async fn get_model_customization_job_uses_get_and_encoded_path() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let endpoint = format!("http://{}", listener.local_addr().expect("local address")); + let captured: Arc> = Arc::new(Mutex::new(String::new())); + let sink = captured.clone(); + + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept request"); + let (headers, _body) = read_http_request(&mut stream); + *sink.lock().expect("lock") = headers; + let resp_body = r#"{"status":"Completed","outputModelArn":"arn:aws:bedrock:us-east-1:123456789012:custom-model/amazon.nova-lite-v1:0/abcdef012345"}"#; + write!( + stream, + "HTTP/1.1 200 OK\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", + resp_body.len(), + resp_body + ) + .expect("write response"); + }); + + let client = + BedrockClient::new(Client::new(), AwsCredentialProvider::from_config_sync(test_config(endpoint))); + let job_arn = + "arn:aws:bedrock:us-east-1:123456789012:model-customization-job/amazon.nova-lite-v1:0/abcdef012345"; + let response = client + .get_model_customization_job(job_arn) + .await + .expect("request should succeed"); + server.join().expect("server should finish"); + + assert_eq!(response.status(), ModelCustomizationJobStatus::Completed); + assert_eq!( + response.output_model_arn.as_deref(), + Some("arn:aws:bedrock:us-east-1:123456789012:custom-model/amazon.nova-lite-v1:0/abcdef012345") + ); + + let headers = captured.lock().expect("lock").clone(); + let request_line = headers.lines().next().unwrap_or_default(); + assert!( + request_line.starts_with("GET /model-customization-jobs/"), + "unexpected request line: {request_line}" + ); + // ARN separators must be percent-encoded in the path segment. + assert!( + request_line.contains("%3A") && request_line.contains("%2F"), + "job identifier must be percent-encoded in the path: {request_line}" + ); + } +} diff --git a/crates/alien-aws-clients/src/aws/mod.rs b/crates/alien-aws-clients/src/aws/mod.rs index 48ce254aa..423081cfd 100644 --- a/crates/alien-aws-clients/src/aws/mod.rs +++ b/crates/alien-aws-clients/src/aws/mod.rs @@ -48,6 +48,7 @@ pub mod acm; pub mod apigatewayv2; pub mod autoscaling; pub mod aws_request_utils; +pub mod bedrock; pub mod cloudformation; pub mod cloudwatch; pub mod codebuild; diff --git a/crates/alien-aws-clients/src/lib.rs b/crates/alien-aws-clients/src/lib.rs index 953b21287..946c3449d 100644 --- a/crates/alien-aws-clients/src/lib.rs +++ b/crates/alien-aws-clients/src/lib.rs @@ -8,6 +8,7 @@ pub use aws::{AwsClientConfig, AwsClientConfigExt, AwsImpersonationConfig}; // Re-export all client APIs pub use aws::acm::{AcmApi, AcmClient}; pub use aws::apigatewayv2::{ApiGatewayV2Api, ApiGatewayV2Client}; +pub use aws::bedrock::{BedrockApi, BedrockClient}; pub use aws::cloudformation::{CloudFormationApi, CloudFormationClient}; pub use aws::cloudwatch::{CloudWatchApi, CloudWatchClient}; pub use aws::codebuild::{CodeBuildApi, CodeBuildClient}; diff --git a/crates/alien-azure-clients/src/azure/cognitive_services.rs b/crates/alien-azure-clients/src/azure/cognitive_services.rs new file mode 100644 index 000000000..971ee964d --- /dev/null +++ b/crates/alien-azure-clients/src/azure/cognitive_services.rs @@ -0,0 +1,666 @@ +use crate::azure::common::{AzureClientBase, AzureRequestBuilder}; +use crate::azure::long_running_operation::OperationResult; +use crate::azure::token_cache::AzureTokenCache; +use alien_client_core::{ErrorData, Result}; +use alien_error::{Context, IntoAlienError}; +use reqwest::{Client, Method}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test-utils")] +use mockall::automock; + +const COGNITIVE_SERVICES_API_VERSION: &str = "2024-10-01"; + +// ------------------------------------------------------------------------- +// ARM resource models +// ------------------------------------------------------------------------- + +/// Properties of a CognitiveServices account +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesAccountProperties { + /// The endpoint URL of the account once provisioned + pub endpoint: Option, + /// The provisioning state of the account + pub provisioning_state: Option, +} + +/// SKU for a CognitiveServices account +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesSku { + /// The SKU name (e.g. "S0") + pub name: String, +} + +/// An Azure CognitiveServices account resource +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesAccount { + /// The ARM resource kind (e.g. "AIServices") + pub kind: Option, + /// The Azure region where the account lives + pub location: Option, + /// The SKU + pub sku: Option, + /// The account properties, including endpoint and provisioning state + pub properties: Option, +} + +/// Request body for creating a CognitiveServices account +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesAccountCreateParameters { + /// Azure region + pub location: String, + /// Account kind — must be "AIServices" for Azure AI Services + pub kind: String, + /// SKU + pub sku: CognitiveServicesSku, + /// Account properties + pub properties: CognitiveServicesAccountCreateProperties, +} + +/// Properties included in the create request body +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesAccountCreateProperties { + /// Custom subdomain name used to build the endpoint URL + pub custom_sub_domain_name: String, +} + +/// Create may complete synchronously or return an Azure long-running operation. +pub type CognitiveServicesAccountOperationResult = OperationResult; + +/// The model a deployment serves. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesDeploymentModel { + /// Model format (e.g. "OpenAI") + pub format: String, + /// Model name (e.g. "gpt-4.1") + pub name: String, + /// Model version (e.g. "2025-04-14") + pub version: String, +} + +/// SKU (throughput tier + capacity) of a deployment. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesDeploymentSku { + /// SKU name (e.g. "GlobalStandard") + pub name: String, + /// Provisioned throughput units + pub capacity: i32, +} + +/// Properties of a deployment. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesDeploymentProperties { + /// The deployed model + pub model: CognitiveServicesDeploymentModel, + /// The provisioning state (response only) + pub provisioning_state: Option, +} + +/// A CognitiveServices model deployment resource. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesDeployment { + /// The SKU + pub sku: Option, + /// The deployment properties, including the model and provisioning state + pub properties: Option, +} + +/// Properties included in the deployment create request body. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesDeploymentCreateProperties { + /// The model to deploy + pub model: CognitiveServicesDeploymentModel, +} + +/// Request body for creating a deployment (PUT). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitiveServicesDeploymentCreateParameters { + /// The SKU (throughput tier + capacity) + pub sku: CognitiveServicesDeploymentSku, + /// The deployment properties + pub properties: CognitiveServicesDeploymentCreateProperties, +} + +/// Deployment create may complete synchronously or return a long-running operation. +pub type CognitiveServicesDeploymentOperationResult = OperationResult; + +// ------------------------------------------------------------------------- +// CognitiveServices Accounts API trait +// ------------------------------------------------------------------------- + +#[cfg_attr(feature = "test-utils", automock)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +pub trait CognitiveServicesAccountsApi: Send + Sync + std::fmt::Debug { + /// Create a CognitiveServices account (PUT). May return a long-running operation. + async fn create_account( + &self, + resource_group_name: &str, + account_name: &str, + parameters: &CognitiveServicesAccountCreateParameters, + ) -> Result; + + /// Get the properties of a CognitiveServices account. + async fn get_account( + &self, + resource_group_name: &str, + account_name: &str, + ) -> Result; + + /// Delete a CognitiveServices account. + async fn delete_account( + &self, + resource_group_name: &str, + account_name: &str, + ) -> Result<()>; + + /// Create (or update) a model deployment under an account (PUT). May return a + /// long-running operation. + async fn create_deployment( + &self, + resource_group_name: &str, + account_name: &str, + deployment_name: &str, + parameters: &CognitiveServicesDeploymentCreateParameters, + ) -> Result; + + /// Get a model deployment's properties, including its provisioning state. + async fn get_deployment( + &self, + resource_group_name: &str, + account_name: &str, + deployment_name: &str, + ) -> Result; +} + +// ------------------------------------------------------------------------- +// CognitiveServices Accounts client struct +// ------------------------------------------------------------------------- + +#[derive(Debug)] +pub struct AzureCognitiveServicesClient { + pub base: AzureClientBase, + pub token_cache: AzureTokenCache, +} + +impl AzureCognitiveServicesClient { + pub fn new(client: Client, token_cache: AzureTokenCache) -> Self { + let endpoint = token_cache.management_endpoint().to_string(); + Self { + base: AzureClientBase::with_client_config( + client, + endpoint, + token_cache.config().clone(), + ), + token_cache, + } + } + + fn resource_url(&self, resource_group_name: &str, account_name: &str) -> String { + format!( + "/subscriptions/{}/resourceGroups/{}/providers/Microsoft.CognitiveServices/accounts/{}", + self.token_cache.config().subscription_id, + resource_group_name, + account_name + ) + } + + fn deployment_url( + &self, + resource_group_name: &str, + account_name: &str, + deployment_name: &str, + ) -> String { + format!( + "{}/deployments/{}", + self.resource_url(resource_group_name, account_name), + deployment_name + ) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl CognitiveServicesAccountsApi for AzureCognitiveServicesClient { + /// Create a CognitiveServices account + async fn create_account( + &self, + resource_group_name: &str, + account_name: &str, + parameters: &CognitiveServicesAccountCreateParameters, + ) -> Result { + let bearer_token = self + .token_cache + .get_bearer_token_with_scope("https://management.azure.com/.default") + .await?; + + let url = self.base.build_url( + &self.resource_url(resource_group_name, account_name), + Some(vec![("api-version", COGNITIVE_SERVICES_API_VERSION.into())]), + ); + + let body = serde_json::to_string(parameters) + .into_alien_error() + .context(ErrorData::SerializationError { + message: format!( + "Failed to serialize CognitiveServices account create parameters for resource: {}", + account_name + ), + })?; + + let builder = AzureRequestBuilder::new(Method::PUT, url) + .content_type_json() + .content_length(&body) + .body(body); + + let req = builder.build()?; + let signed = self.base.sign_request(req, &bearer_token).await?; + + self.base + .execute_request_with_long_running_support(signed, "CreateCognitiveServicesAccount", account_name) + .await + } + + /// Get a CognitiveServices account's properties + async fn get_account( + &self, + resource_group_name: &str, + account_name: &str, + ) -> Result { + let bearer_token = self + .token_cache + .get_bearer_token_with_scope("https://management.azure.com/.default") + .await?; + + let url = self.base.build_url( + &self.resource_url(resource_group_name, account_name), + Some(vec![("api-version", COGNITIVE_SERVICES_API_VERSION.into())]), + ); + + let builder = AzureRequestBuilder::new(Method::GET, url.clone()).content_length(""); + + let req = builder.build()?; + let signed = self.base.sign_request(req, &bearer_token).await?; + let resp = self + .base + .execute_request(signed, "GetCognitiveServicesAccount", account_name) + .await?; + + let body = resp + .text() + .await + .into_alien_error() + .context(ErrorData::HttpResponseError { + message: format!( + "Azure GetCognitiveServicesAccount: failed to read response body for {}", + account_name + ), + url: url.clone(), + http_status: 200, + http_request_text: None, + http_response_text: None, + })?; + + let account: CognitiveServicesAccount = serde_json::from_str(&body) + .into_alien_error() + .context(ErrorData::HttpResponseError { + message: format!( + "Azure GetCognitiveServicesAccount: JSON parse error for {}", + account_name + ), + url: url.clone(), + http_status: 200, + http_request_text: None, + http_response_text: Some(body), + })?; + + Ok(account) + } + + /// Delete a CognitiveServices account + async fn delete_account( + &self, + resource_group_name: &str, + account_name: &str, + ) -> Result<()> { + let bearer_token = self + .token_cache + .get_bearer_token_with_scope("https://management.azure.com/.default") + .await?; + + let url = self.base.build_url( + &self.resource_url(resource_group_name, account_name), + Some(vec![("api-version", COGNITIVE_SERVICES_API_VERSION.into())]), + ); + + let builder = AzureRequestBuilder::new(Method::DELETE, url).content_length(""); + + let req = builder.build()?; + let signed = self.base.sign_request(req, &bearer_token).await?; + let _resp = self + .base + .execute_request(signed, "DeleteCognitiveServicesAccount", account_name) + .await?; + + Ok(()) + } + + /// Create (or update) a model deployment under an account + async fn create_deployment( + &self, + resource_group_name: &str, + account_name: &str, + deployment_name: &str, + parameters: &CognitiveServicesDeploymentCreateParameters, + ) -> Result { + let bearer_token = self + .token_cache + .get_bearer_token_with_scope("https://management.azure.com/.default") + .await?; + + let url = self.base.build_url( + &self.deployment_url(resource_group_name, account_name, deployment_name), + Some(vec![("api-version", COGNITIVE_SERVICES_API_VERSION.into())]), + ); + + let body = serde_json::to_string(parameters) + .into_alien_error() + .context(ErrorData::SerializationError { + message: format!( + "Failed to serialize CognitiveServices deployment create parameters for: {}", + deployment_name + ), + })?; + + let builder = AzureRequestBuilder::new(Method::PUT, url) + .content_type_json() + .content_length(&body) + .body(body); + + let req = builder.build()?; + let signed = self.base.sign_request(req, &bearer_token).await?; + + self.base + .execute_request_with_long_running_support( + signed, + "CreateCognitiveServicesDeployment", + deployment_name, + ) + .await + } + + /// Get a model deployment's properties + async fn get_deployment( + &self, + resource_group_name: &str, + account_name: &str, + deployment_name: &str, + ) -> Result { + let bearer_token = self + .token_cache + .get_bearer_token_with_scope("https://management.azure.com/.default") + .await?; + + let url = self.base.build_url( + &self.deployment_url(resource_group_name, account_name, deployment_name), + Some(vec![("api-version", COGNITIVE_SERVICES_API_VERSION.into())]), + ); + + let builder = AzureRequestBuilder::new(Method::GET, url.clone()).content_length(""); + + let req = builder.build()?; + let signed = self.base.sign_request(req, &bearer_token).await?; + let resp = self + .base + .execute_request(signed, "GetCognitiveServicesDeployment", deployment_name) + .await?; + + let body = resp + .text() + .await + .into_alien_error() + .context(ErrorData::HttpResponseError { + message: format!( + "Azure GetCognitiveServicesDeployment: failed to read response body for {}", + deployment_name + ), + url: url.clone(), + http_status: 200, + http_request_text: None, + http_response_text: None, + })?; + + let deployment: CognitiveServicesDeployment = serde_json::from_str(&body) + .into_alien_error() + .context(ErrorData::HttpResponseError { + message: format!( + "Azure GetCognitiveServicesDeployment: JSON parse error for {}", + deployment_name + ), + url: url.clone(), + http_status: 200, + http_request_text: None, + http_response_text: Some(body), + })?; + + Ok(deployment) + } +} + +// ------------------------------------------------------------------------- +// Tests +// ------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uses_current_cognitive_services_api_version() { + assert_eq!(COGNITIVE_SERVICES_API_VERSION, "2024-10-01"); + } + + /// Verifies that the ARM GET response for a CognitiveServices account deserializes + /// correctly into our hand-written model. This catches camelCase/field-name mistakes. + #[test] + fn deserializes_arm_get_response() { + let json = r#"{ + "kind": "AIServices", + "location": "eastus", + "sku": { "name": "S0" }, + "properties": { + "endpoint": "https://my-account.cognitiveservices.azure.com/", + "provisioningState": "Succeeded" + } + }"#; + + let account: CognitiveServicesAccount = + serde_json::from_str(json).expect("ARM GET response should deserialize"); + + assert_eq!(account.kind.as_deref(), Some("AIServices")); + assert_eq!(account.location.as_deref(), Some("eastus")); + assert_eq!(account.sku.as_ref().map(|s| s.name.as_str()), Some("S0")); + + let props = account.properties.expect("properties should be present"); + assert_eq!( + props.endpoint.as_deref(), + Some("https://my-account.cognitiveservices.azure.com/") + ); + assert_eq!(props.provisioning_state.as_deref(), Some("Succeeded")); + } + + /// Verifies that the create parameters serialize to the expected ARM request body shape. + #[test] + fn serializes_create_parameters() { + let params = CognitiveServicesAccountCreateParameters { + location: "eastus".to_string(), + kind: "AIServices".to_string(), + sku: CognitiveServicesSku { + name: "S0".to_string(), + }, + properties: CognitiveServicesAccountCreateProperties { + custom_sub_domain_name: "my-account".to_string(), + }, + }; + + let json = serde_json::to_string(¶ms).expect("should serialize"); + let value: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert_eq!(value["kind"], "AIServices"); + assert_eq!(value["location"], "eastus"); + assert_eq!(value["sku"]["name"], "S0"); + assert_eq!(value["properties"]["customSubDomainName"], "my-account"); + } + + /// The deployment create body must match the ARM shape: sku {name,capacity} and + /// properties.model {format,name,version}. + #[test] + fn serializes_deployment_create_parameters() { + let params = CognitiveServicesDeploymentCreateParameters { + sku: CognitiveServicesDeploymentSku { + name: "GlobalStandard".to_string(), + capacity: 1, + }, + properties: CognitiveServicesDeploymentCreateProperties { + model: CognitiveServicesDeploymentModel { + format: "OpenAI".to_string(), + name: "gpt-4.1".to_string(), + version: "2025-04-14".to_string(), + }, + }, + }; + + let value: serde_json::Value = + serde_json::from_str(&serde_json::to_string(¶ms).unwrap()).unwrap(); + + assert_eq!(value["sku"]["name"], "GlobalStandard"); + assert_eq!(value["sku"]["capacity"], 1); + assert_eq!(value["properties"]["model"]["format"], "OpenAI"); + assert_eq!(value["properties"]["model"]["name"], "gpt-4.1"); + assert_eq!(value["properties"]["model"]["version"], "2025-04-14"); + } + + /// The ARM GET response for a deployment must deserialize, including the + /// provisioning state the controller polls on. + #[test] + fn deserializes_deployment_get_response() { + let json = r#"{ + "sku": { "name": "GlobalStandard", "capacity": 1 }, + "properties": { + "model": { "format": "OpenAI", "name": "gpt-4.1", "version": "2025-04-14" }, + "provisioningState": "Succeeded" + } + }"#; + + let deployment: CognitiveServicesDeployment = + serde_json::from_str(json).expect("ARM GET deployment response should deserialize"); + + let props = deployment.properties.expect("properties should be present"); + assert_eq!(props.model.name, "gpt-4.1"); + assert_eq!(props.model.version, "2025-04-14"); + assert_eq!(props.provisioning_state.as_deref(), Some("Succeeded")); + } +} + +#[cfg(all(test, feature = "test-utils"))] +mod mock_tests { + use super::*; + + /// Happy-path round-trip: create, get, delete using the mock. Confirms the mock + /// generated by automock compiles and satisfies the trait surface. + #[tokio::test] + async fn mock_create_get_delete_round_trip() { + let mut mock = MockCognitiveServicesAccountsApi::new(); + + mock.expect_create_account() + .returning(|_, _, _| { + Ok(OperationResult::Completed(CognitiveServicesAccount { + kind: Some("AIServices".to_string()), + location: Some("eastus".to_string()), + sku: Some(CognitiveServicesSku { + name: "S0".to_string(), + }), + properties: Some(CognitiveServicesAccountProperties { + endpoint: Some( + "https://my-account.cognitiveservices.azure.com/".to_string(), + ), + provisioning_state: Some("Succeeded".to_string()), + }), + })) + }); + + mock.expect_get_account().returning(|_, _| { + Ok(CognitiveServicesAccount { + kind: Some("AIServices".to_string()), + location: Some("eastus".to_string()), + sku: Some(CognitiveServicesSku { + name: "S0".to_string(), + }), + properties: Some(CognitiveServicesAccountProperties { + endpoint: Some( + "https://my-account.cognitiveservices.azure.com/".to_string(), + ), + provisioning_state: Some("Succeeded".to_string()), + }), + }) + }); + + mock.expect_delete_account().returning(|_, _| Ok(())); + + let params = CognitiveServicesAccountCreateParameters { + location: "eastus".to_string(), + kind: "AIServices".to_string(), + sku: CognitiveServicesSku { + name: "S0".to_string(), + }, + properties: CognitiveServicesAccountCreateProperties { + custom_sub_domain_name: "my-account".to_string(), + }, + }; + + let create_result = mock + .create_account("my-rg", "my-account", ¶ms) + .await + .expect("create should succeed"); + + let account = match create_result { + OperationResult::Completed(a) => a, + OperationResult::LongRunning(_) => panic!("expected completed result from mock"), + }; + + let props = account.properties.expect("properties should be present"); + assert_eq!( + props.endpoint.as_deref(), + Some("https://my-account.cognitiveservices.azure.com/") + ); + assert_eq!(props.provisioning_state.as_deref(), Some("Succeeded")); + + let gotten = mock + .get_account("my-rg", "my-account") + .await + .expect("get should succeed"); + + assert_eq!( + gotten + .properties + .as_ref() + .and_then(|p| p.endpoint.as_deref()), + Some("https://my-account.cognitiveservices.azure.com/") + ); + + mock.delete_account("my-rg", "my-account") + .await + .expect("delete should succeed"); + } +} diff --git a/crates/alien-azure-clients/src/azure/mod.rs b/crates/alien-azure-clients/src/azure/mod.rs index 06cf6a6f6..8116be840 100644 --- a/crates/alien-azure-clients/src/azure/mod.rs +++ b/crates/alien-azure-clients/src/azure/mod.rs @@ -12,6 +12,7 @@ pub use alien_core::{ pub mod application_gateways; pub mod authorization; pub mod blob_containers; +pub mod cognitive_services; pub mod common; pub mod compute; pub mod container_apps; @@ -27,6 +28,7 @@ pub mod managed_identity; pub mod models; pub mod monitor; pub mod network; +pub mod openai_finetuning; pub mod private_networking; pub mod resource_graph; pub mod resource_skus; diff --git a/crates/alien-azure-clients/src/azure/openai_finetuning.rs b/crates/alien-azure-clients/src/azure/openai_finetuning.rs new file mode 100644 index 000000000..7bff1e2a2 --- /dev/null +++ b/crates/alien-azure-clients/src/azure/openai_finetuning.rs @@ -0,0 +1,434 @@ +//! Azure AI Foundry (Azure OpenAI) fine-tuning — a **data-plane** client. +//! +//! Unlike [`cognitive_services`](crate::azure::cognitive_services), which drives +//! the ARM/control-plane to provision the account and its deployments, this +//! client talks to the *account's own OpenAI data-plane endpoint* +//! (`https://{account}.openai.azure.com` / `.cognitiveservices.azure.com`). +//! +//! Two differences from the control-plane client: +//! - The base URL is the **account endpoint**, supplied per call by the caller +//! (the controller already learns it when the account is provisioned). There +//! is no subscription/resource-group path. +//! - The bearer token is minted for the **cognitive-services data-plane scope** +//! `https://cognitiveservices.azure.com/.default`, not +//! `https://management.azure.com/.default`. Data-plane RBAC (e.g. the +//! `Cognitive Services OpenAI Contributor` role the controller applies) is +//! distinct from ARM Contributor. +//! +//! REST surface (Azure OpenAI fine-tuning, API version `2024-10-21`): +//! - `POST {endpoint}/openai/fine_tuning/jobs?api-version=...` +//! - `GET {endpoint}/openai/fine_tuning/jobs/{job_id}?api-version=...` + +use crate::azure::common::{AzureClientBase, AzureRequestBuilder}; +use crate::azure::token_cache::AzureTokenCache; +use alien_client_core::{ErrorData, Result}; +use alien_error::{Context, IntoAlienError}; +use reqwest::{Client, Method}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test-utils")] +use mockall::automock; + +/// Fine-tuning REST API version. Azure pins the whole `fine_tuning.jobs` +/// surface to a dated version supplied as an `api-version` query parameter. +pub const OPENAI_FINE_TUNING_API_VERSION: &str = "2024-10-21"; + +/// OAuth scope for the Azure Cognitive Services **data plane**. +/// +/// Distinct from the ARM management scope (`https://management.azure.com/.default`) +/// used by the control-plane client: fine-tuning is a data action authorized by +/// the data-plane RBAC role assigned on the account. +pub const COGNITIVE_SERVICES_DATA_PLANE_SCOPE: &str = "https://cognitiveservices.azure.com/.default"; + +// ------------------------------------------------------------------------- +// Data-plane models +// ------------------------------------------------------------------------- + +/// Request body for `POST /openai/fine_tuning/jobs`. +/// +/// `training_file` is the identifier Azure OpenAI accepts for the dataset. For +/// a Foundry import from customer Blob storage this is the blob reference; see +/// [`FoundryFineTuningApi::create_fine_tuning_job`] for the network gotcha. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct FineTuningJobCreateRequest { + /// Provider-native base model to fine-tune (e.g. `gpt-4o-mini`). + pub model: String, + /// The training dataset reference the job reads from. + pub training_file: String, +} + +/// A fine-tuning job as returned by `POST` (creation) and `GET` (poll). +/// +/// Field names are Azure OpenAI's native `snake_case`, so this type does **not** +/// use the workspace's usual `camelCase` rename — matching the wire shape is the +/// whole point. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FineTuningJob { + /// The job id (e.g. `ftjob-abc123`). Stored by the controller to poll. + pub id: String, + /// Lifecycle status: `pending`, `validating_files`, `queued`, `running`, + /// `succeeded`, `failed`, or `cancelled`. + pub status: String, + /// Populated only once `status == "succeeded"`: the tuned model name the + /// OpenAI chat endpoint accepts as a deployment target. + #[serde(default)] + pub fine_tuned_model: Option, +} + +impl FineTuningJob { + /// Whether the job reached the successful terminal state. + pub fn is_succeeded(&self) -> bool { + self.status.eq_ignore_ascii_case("succeeded") + } + + /// Whether the job reached a terminal *failure* state (`failed`/`cancelled`). + /// The controller fails fast on these rather than polling forever. + pub fn is_terminal_failure(&self) -> bool { + self.status.eq_ignore_ascii_case("failed") + || self.status.eq_ignore_ascii_case("cancelled") + || self.status.eq_ignore_ascii_case("canceled") + } +} + +// ------------------------------------------------------------------------- +// Foundry fine-tuning API trait +// ------------------------------------------------------------------------- + +/// Data-plane fine-tuning operations against an Azure AI Foundry account. +/// +/// The `endpoint` argument is the account's OpenAI data-plane base URL (e.g. +/// `https://my-account.openai.azure.com`), learned by the controller when the +/// account is provisioned. +#[cfg_attr(feature = "test-utils", automock)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +pub trait FoundryFineTuningApi: Send + Sync + std::fmt::Debug { + /// Submit a fine-tuning job (`POST /openai/fine_tuning/jobs`). + /// + /// `training_file` is the dataset reference the job reads. Foundry supports + /// importing directly from customer Blob storage, but **that import path + /// requires the account to allow public network access** — a private-endpoint + /// account will reject the blob URL at submit time. This client passes the + /// caller's reference through unchanged; enforcing network posture is the + /// controller/account concern. + async fn create_fine_tuning_job( + &self, + endpoint: &str, + model: &str, + training_file: &str, + ) -> Result; + + /// Poll a fine-tuning job's status (`GET /openai/fine_tuning/jobs/{job_id}`). + /// On success the returned job carries `fine_tuned_model`. + async fn get_fine_tuning_job(&self, endpoint: &str, job_id: &str) -> Result; +} + +// ------------------------------------------------------------------------- +// Foundry fine-tuning client struct +// ------------------------------------------------------------------------- + +#[derive(Debug)] +pub struct AzureFoundryFineTuningClient { + pub base: AzureClientBase, + pub token_cache: AzureTokenCache, +} + +impl AzureFoundryFineTuningClient { + pub fn new(client: Client, token_cache: AzureTokenCache) -> Self { + // The base endpoint is supplied per call (the account endpoint), so the + // base's own endpoint field is unused for URL building here. Seed it with + // the management endpoint for parity with the other clients. + let endpoint = token_cache.management_endpoint().to_string(); + Self { + base: AzureClientBase::with_client_config( + client, + endpoint, + token_cache.config().clone(), + ), + token_cache, + } + } + + /// Builds `{endpoint}/openai/fine_tuning/jobs[/{job_id}]?api-version=...`. + fn jobs_url(&self, endpoint: &str, job_id: Option<&str>) -> String { + let base = endpoint.trim_end_matches('/'); + let path = match job_id { + Some(id) => format!("{base}/openai/fine_tuning/jobs/{id}"), + None => format!("{base}/openai/fine_tuning/jobs"), + }; + format!("{path}?api-version={OPENAI_FINE_TUNING_API_VERSION}") + } + + /// Reads and JSON-parses a `FineTuningJob` from a successful response. + async fn parse_job(resp: reqwest::Response, url: &str, op: &str) -> Result { + let status = resp.status().as_u16(); + let body = resp + .text() + .await + .into_alien_error() + .context(ErrorData::HttpResponseError { + message: format!("Azure {op}: failed to read fine-tuning job response body"), + url: url.to_string(), + http_status: status, + http_request_text: None, + http_response_text: None, + })?; + + serde_json::from_str(&body) + .into_alien_error() + .context(ErrorData::HttpResponseError { + message: format!("Azure {op}: JSON parse error for fine-tuning job"), + url: url.to_string(), + http_status: status, + http_request_text: None, + http_response_text: Some(body), + }) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl FoundryFineTuningApi for AzureFoundryFineTuningClient { + async fn create_fine_tuning_job( + &self, + endpoint: &str, + model: &str, + training_file: &str, + ) -> Result { + let bearer_token = self + .token_cache + .get_bearer_token_with_scope(COGNITIVE_SERVICES_DATA_PLANE_SCOPE) + .await?; + + let url = self.jobs_url(endpoint, None); + + let request = FineTuningJobCreateRequest { + model: model.to_string(), + training_file: training_file.to_string(), + }; + let body = serde_json::to_string(&request) + .into_alien_error() + .context(ErrorData::SerializationError { + message: format!( + "Failed to serialize fine-tuning job create request for model '{model}'" + ), + })?; + + let builder = AzureRequestBuilder::new(Method::POST, url.clone()) + .content_type_json() + .content_length(&body) + .body(body); + + let req = builder.build()?; + let signed = self.base.sign_request(req, &bearer_token).await?; + let resp = self + .base + .execute_request(signed, "CreateFineTuningJob", model) + .await?; + + Self::parse_job(resp, &url, "CreateFineTuningJob").await + } + + async fn get_fine_tuning_job(&self, endpoint: &str, job_id: &str) -> Result { + let bearer_token = self + .token_cache + .get_bearer_token_with_scope(COGNITIVE_SERVICES_DATA_PLANE_SCOPE) + .await?; + + let url = self.jobs_url(endpoint, Some(job_id)); + + let builder = AzureRequestBuilder::new(Method::GET, url.clone()).content_length(""); + let req = builder.build()?; + let signed = self.base.sign_request(req, &bearer_token).await?; + let resp = self + .base + .execute_request(signed, "GetFineTuningJob", job_id) + .await?; + + Self::parse_job(resp, &url, "GetFineTuningJob").await + } +} + +// ------------------------------------------------------------------------- +// Tests +// ------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uses_pinned_api_version_and_data_plane_scope() { + assert_eq!(OPENAI_FINE_TUNING_API_VERSION, "2024-10-21"); + // The data-plane scope must NOT be the ARM management scope — a token for + // the wrong audience is rejected by Azure. + assert_eq!( + COGNITIVE_SERVICES_DATA_PLANE_SCOPE, + "https://cognitiveservices.azure.com/.default" + ); + assert_ne!( + COGNITIVE_SERVICES_DATA_PLANE_SCOPE, + "https://management.azure.com/.default" + ); + } + + #[test] + fn create_request_serializes_snake_case_wire_shape() { + let req = FineTuningJobCreateRequest { + model: "gpt-4o-mini".to_string(), + training_file: "https://acct.blob.core.windows.net/data/training.jsonl".to_string(), + }; + let value: serde_json::Value = + serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap(); + assert_eq!(value["model"], "gpt-4o-mini"); + assert_eq!( + value["training_file"], + "https://acct.blob.core.windows.net/data/training.jsonl" + ); + } + + #[test] + fn job_status_helpers_classify_terminal_states() { + let succeeded = FineTuningJob { + id: "ftjob-1".to_string(), + status: "succeeded".to_string(), + fine_tuned_model: Some("gpt-4o-mini.ft-1".to_string()), + }; + assert!(succeeded.is_succeeded()); + assert!(!succeeded.is_terminal_failure()); + + for failed in ["failed", "cancelled", "canceled", "FAILED"] { + let job = FineTuningJob { + id: "ftjob-2".to_string(), + status: failed.to_string(), + fine_tuned_model: None, + }; + assert!(job.is_terminal_failure(), "'{failed}' must be terminal failure"); + assert!(!job.is_succeeded()); + } + + for pending in ["pending", "running", "queued", "validating_files"] { + let job = FineTuningJob { + id: "ftjob-3".to_string(), + status: pending.to_string(), + fine_tuned_model: None, + }; + assert!(!job.is_succeeded(), "'{pending}' is not succeeded"); + assert!(!job.is_terminal_failure(), "'{pending}' is not terminal failure"); + } + } + + #[test] + fn deserializes_get_response_with_fine_tuned_model() { + // Azure's GET response once the job succeeds. + let json = r#"{ + "id": "ftjob-abc123", + "status": "succeeded", + "model": "gpt-4o-mini", + "fine_tuned_model": "gpt-4o-mini.ft-abc123", + "object": "fine_tuning.job" + }"#; + let job: FineTuningJob = serde_json::from_str(json).expect("should deserialize"); + assert_eq!(job.id, "ftjob-abc123"); + assert!(job.is_succeeded()); + assert_eq!(job.fine_tuned_model.as_deref(), Some("gpt-4o-mini.ft-abc123")); + } + + #[test] + fn deserializes_pending_response_without_fine_tuned_model() { + // A freshly-created job has no fine_tuned_model yet. + let json = r#"{ "id": "ftjob-abc123", "status": "pending", "object": "fine_tuning.job" }"#; + let job: FineTuningJob = serde_json::from_str(json).expect("should deserialize"); + assert_eq!(job.status, "pending"); + assert!(job.fine_tuned_model.is_none()); + } +} + +#[cfg(all(test, feature = "test-utils"))] +mod http_tests { + use super::*; + use crate::azure::{AzureClientConfig, AzureClientConfigExt}; + use httpmock::{Method::GET, Method::POST, MockServer}; + use serde_json::json; + + fn test_client() -> AzureFoundryFineTuningClient { + // The account endpoint is passed per call, so the client itself needs no + // endpoint override — the MockServer URL is the `endpoint` argument. + AzureFoundryFineTuningClient::new( + Client::new(), + AzureTokenCache::new(AzureClientConfig::mock()), + ) + } + + /// The POST must hit the data-plane fine-tuning path with the pinned + /// api-version, carry a bearer token, and send the snake_case body. This is + /// the request-shape contract the gateway/controller relies on. + #[tokio::test] + async fn create_job_builds_correct_request() { + let server = MockServer::start_async().await; + + let request_mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/openai/fine_tuning/jobs") + .query_param("api-version", OPENAI_FINE_TUNING_API_VERSION) + .header_exists("authorization") + .json_body(json!({ + "model": "gpt-4o-mini", + "training_file": "https://acct.blob.core.windows.net/data/training.jsonl" + })); + then.status(201).json_body(json!({ + "id": "ftjob-xyz", + "status": "pending", + "object": "fine_tuning.job" + })); + }) + .await; + + let job = test_client() + .create_fine_tuning_job( + &server.base_url(), + "gpt-4o-mini", + "https://acct.blob.core.windows.net/data/training.jsonl", + ) + .await + .expect("create fine-tuning job should succeed"); + + request_mock.assert_async().await; + assert_eq!(job.id, "ftjob-xyz"); + assert_eq!(job.status, "pending"); + assert!(job.fine_tuned_model.is_none()); + } + + /// The GET must hit `/openai/fine_tuning/jobs/{job_id}` and parse the tuned + /// model name out of a succeeded response. + #[tokio::test] + async fn get_job_builds_correct_request_and_parses_success() { + let server = MockServer::start_async().await; + + let request_mock = server + .mock_async(|when, then| { + when.method(GET) + .path("/openai/fine_tuning/jobs/ftjob-xyz") + .query_param("api-version", OPENAI_FINE_TUNING_API_VERSION) + .header_exists("authorization"); + then.status(200).json_body(json!({ + "id": "ftjob-xyz", + "status": "succeeded", + "fine_tuned_model": "gpt-4o-mini.ft-xyz", + "object": "fine_tuning.job" + })); + }) + .await; + + let job = test_client() + .get_fine_tuning_job(&server.base_url(), "ftjob-xyz") + .await + .expect("get fine-tuning job should succeed"); + + request_mock.assert_async().await; + assert!(job.is_succeeded()); + assert_eq!(job.fine_tuned_model.as_deref(), Some("gpt-4o-mini.ft-xyz")); + } +} diff --git a/crates/alien-azure-clients/src/lib.rs b/crates/alien-azure-clients/src/lib.rs index 21e266578..feff96b8a 100644 --- a/crates/alien-azure-clients/src/lib.rs +++ b/crates/alien-azure-clients/src/lib.rs @@ -9,6 +9,9 @@ pub use azure::{ // Re-export all client APIs pub use azure::application_gateways::{ApplicationGatewayApi, AzureApplicationGatewayClient}; +pub use azure::cognitive_services::{ + AzureCognitiveServicesClient, CognitiveServicesAccountsApi, +}; pub use azure::authorization::{AuthorizationApi, AzureAuthorizationClient}; pub use azure::blob_containers::{AzureBlobContainerClient, BlobContainerApi}; pub use azure::compute::{AzureVmssClient, VirtualMachineScaleSetsApi}; @@ -30,6 +33,9 @@ pub use azure::managed_identity::{ }; pub use azure::monitor::{AzureMonitorClient, MonitorApi}; pub use azure::network::{AzureNetworkClient, NetworkApi}; +pub use azure::openai_finetuning::{ + AzureFoundryFineTuningClient, FineTuningJob, FineTuningJobCreateRequest, FoundryFineTuningApi, +}; pub use azure::resource_graph::{AzureResourceGraphClient, ResourceGraphApi}; pub use azure::resource_skus::{ AzureResourceSkusClient, ResourceSku, ResourceSkuLocationInfo, ResourceSkuRestriction, diff --git a/crates/alien-bindings/src/provider.rs b/crates/alien-bindings/src/provider.rs index 4a9c7a72e..81394c91a 100644 --- a/crates/alien-bindings/src/provider.rs +++ b/crates/alien-bindings/src/provider.rs @@ -117,6 +117,14 @@ impl BindingsProvider { }) } + /// The workload's resolved cloud credentials (native/projected identity or Alien-minted + /// short-lived credentials). Consumers that need to authorize a request themselves — the + /// AI gateway signs upstream model calls with this — read it here rather than going + /// through a per-resource binding. + pub fn client_config(&self) -> &ClientConfig { + &self.client_config + } + /// Get a cached binding by type and name, or return None. async fn get_cached( &self, @@ -468,7 +476,7 @@ impl LazyEnvBindingsProvider { /// The strategy selection happens exactly once (via `OnceCell`); on the /// minting path each call additionally checks credential freshness and /// re-mints on access if stale. - async fn provider(&self) -> Result> { + pub async fn provider(&self) -> Result> { let resolver = self .resolver .get_or_try_init(|| async { self.select().await }) diff --git a/crates/alien-bindings/src/traits.rs b/crates/alien-bindings/src/traits.rs index 9770f820d..d200211e0 100644 --- a/crates/alien-bindings/src/traits.rs +++ b/crates/alien-bindings/src/traits.rs @@ -825,13 +825,15 @@ pub trait BindingsProviderApi: Send + Sync + std::fmt::Debug { /// Given a binding identifier, builds a ServiceAccount implementation. async fn load_service_account(&self, binding_name: &str) -> Result>; - /// Runtime-only binding env vars (a local Postgres connection with its password) for the given - /// resource — re-resolved on every (re)start so the secret reaches the worker process but is - /// never written to persisted worker metadata. Default `None`: cloud providers carry a secret - /// locator (not a password) and use the normal persisted path. + /// Runtime-only binding env vars (a local Postgres connection with its password, a local + /// BYO-key AI binding) for the given resource — re-resolved on every (re)start so the secret + /// reaches the worker process but is never written to persisted worker metadata. The resource + /// type routes resolution to the right local source. Default `None`: cloud providers carry a + /// secret locator (not a raw secret) and use the normal persisted path. async fn resolve_runtime_only_binding_env( &self, _binding_name: &str, + _resource_type: &str, ) -> Result>> { Ok(None) } diff --git a/crates/alien-build/src/toolchain/native_addon.rs b/crates/alien-build/src/toolchain/native_addon.rs index e57bda65a..9fd66078a 100644 --- a/crates/alien-build/src/toolchain/native_addon.rs +++ b/crates/alien-build/src/toolchain/native_addon.rs @@ -28,12 +28,45 @@ use tokio::fs; use tokio::process::Command; use tracing::info; -/// npm package that carries the JS side of the native bindings. -const BINDINGS_PACKAGE: &str = "@alienplatform/bindings"; +/// One napi-addon package that a compiled binary embeds statically. Two ship +/// today — `@alienplatform/bindings` (kv/storage/queue/vault/container) and +/// `@alienplatform/ai-gateway` (the `ai()` client) — and each stages its own +/// addon next to its own `dist/native.js` under the literal file name its +/// `./native` entry imports. +struct NativeAddonSpec { + /// npm package carrying the JS side (e.g. "@alienplatform/bindings"). + package: &'static str, + /// Package name without the `@alienplatform/` scope (e.g. "bindings"), used + /// for the sibling prebuild directory `/-`. + scoped_name: &'static str, + /// File name the package's `./native` entry statically imports + /// (`import addon from "./"` next to `dist/native.js`). + staged_file: &'static str, + /// Workspace crate dir under `crates/` — also the addon file-name prefix + /// (`..node`). + crate_dir: &'static str, +} -/// File name the bindings package's `./native` entry statically imports -/// (`import addon from "./alien-bindings.node"` next to `dist/native.js`). -const STAGED_ADDON_FILE: &str = "alien-bindings.node"; +/// The bindings addon: required whenever the app resolves it (every consumer +/// uses some binding), so a missing addon for the target fails the build. +const BINDINGS: NativeAddonSpec = NativeAddonSpec { + package: "@alienplatform/bindings", + scoped_name: "bindings", + staged_file: "alien-bindings.node", + crate_dir: "alien-bindings-node", +}; + +/// The AI-gateway addon: staged best-effort. A Worker resolves it transitively +/// through the SDK even when it never calls `ai()`, so a missing addon for the +/// target is skipped (not a build error) — requiring it would regress every +/// non-AI Worker. When the addon is present it is embedded so a compiled +/// `ai()`/`getAiConnection()` resolves. +const AI_GATEWAY: NativeAddonSpec = NativeAddonSpec { + package: "@alienplatform/ai-gateway", + scoped_name: "ai-gateway", + staged_file: "alien-ai-gateway.node", + crate_dir: "alien-ai-gateway-node", +}; /// Map a build target to the napi triple used in prebuild package names /// (`@alienplatform/bindings-`) and addon file names @@ -71,19 +104,20 @@ fn napi_triple(target: BinaryTarget) -> Option<&'static str> { /// build error naming the missing prebuild package). async fn find_addon_source( src_dir: &Path, - bindings_dist: &Path, + spec: &NativeAddonSpec, + addon_dist: &Path, triple: &str, addon_file_name: &str, resource_name: &str, checked: &mut Vec, ) -> Result> { - // 1a. Prebuild linked next to the resolved bindings package. `bindings_dist` - // is `/@alienplatform/bindings/dist`, so the prebuild - // `@alienplatform/bindings-` sits at `/@alienplatform/ - // bindings-` — two levels up from dist, then the sibling name. - if let Some(scope_dir) = bindings_dist.parent().and_then(Path::parent) { + // 1a. Prebuild linked next to the resolved package. `addon_dist` is + // `/@alienplatform//dist`, so the prebuild + // `@alienplatform/-` sits at `/@alienplatform/ + // -` — two levels up from dist, then the sibling name. + if let Some(scope_dir) = addon_dist.parent().and_then(Path::parent) { let sibling_prebuild = scope_dir - .join(format!("bindings-{}", triple)) + .join(format!("{}-{}", spec.scoped_name, triple)) .join(addon_file_name); if sibling_prebuild.is_file() { return Ok(Some(sibling_prebuild)); @@ -94,7 +128,7 @@ async fn find_addon_source( // 1b. Prebuild package installed in the app's own node_modules. let prebuild = src_dir .join("node_modules") - .join(format!("{}-{}", BINDINGS_PACKAGE, triple)) + .join(format!("{}-{}", spec.package, triple)) .join(addon_file_name); if prebuild.is_file() { return Ok(Some(prebuild)); @@ -108,12 +142,12 @@ async fn find_addon_source( // only anchor that reaches `crates/alien-bindings-node` in that case. // The dist path is canonicalized so a symlinked node_modules entry // walks the repo, not the app's directory again. - let canonical_dist = bindings_dist.canonicalize().ok(); + let canonical_dist = addon_dist.canonicalize().ok(); let mut workspace_addon_crate: Option = None; 'anchors: for anchor in std::iter::once(src_dir).chain(canonical_dist.as_deref()) { let mut dir = Some(anchor); while let Some(current) = dir { - let crate_dir = current.join("crates").join("alien-bindings-node"); + let crate_dir = current.join("crates").join(spec.crate_dir); if crate_dir.is_dir() { workspace_addon_crate = Some(crate_dir.clone()); let dev_addon = crate_dir.join(addon_file_name); @@ -126,7 +160,8 @@ async fn find_addon_source( dir = current.parent(); } checked.push(format!( - "(no crates/alien-bindings-node above {})", + "(no crates/{} above {})", + spec.crate_dir, anchor.display() )); } @@ -184,56 +219,63 @@ async fn find_addon_source( } } -/// bun script that prints the resolved path of the bindings package's `./native` -/// entry (i.e. `.../@alienplatform/bindings/dist/native.js`) or nothing. +/// bun script that prints the resolved path of a package's `./native` entry +/// (i.e. `.../@alienplatform//dist/native.js`) or nothing. /// -/// It resolves `@alienplatform/bindings/native` first directly from the app -/// (Container/Daemon direct dependency), then — failing that — from the -/// resolved location of `@alienplatform/sdk` (a Worker's only path to the -/// bindings package). Using bun's own resolver honors pnpm symlinks and package -/// `exports` maps, so the printed path is exactly what `bun build --compile` -/// will embed. -const RESOLVE_BINDINGS_NATIVE_SCRIPT: &str = r#" +/// It resolves `/native` first directly from the app (Container/Daemon +/// direct dependency), then — failing that — from the resolved location of +/// `@alienplatform/sdk` (a Worker's only path to the package). Using bun's own +/// resolver honors pnpm symlinks and package `exports` maps, so the printed path +/// is exactly what `bun build --compile` will embed. +fn resolve_native_script(package: &str) -> String { + format!( + r#" const path = require("path"); -const from = process.env.ALIEN_BINDINGS_RESOLVE_FROM; -const tryResolve = (spec, base) => { try { return Bun.resolveSync(spec, base); } catch { return null; } }; +const from = process.env.ALIEN_ADDON_RESOLVE_FROM; +const nativeSpec = "{package}/native"; +const tryResolve = (s, base) => {{ try {{ return Bun.resolveSync(s, base); }} catch {{ return null; }} }}; let route = "direct"; -let nativeEntry = tryResolve("@alienplatform/bindings/native", from); -if (!nativeEntry) { +let nativeEntry = tryResolve(nativeSpec, from); +if (!nativeEntry) {{ const sdkEntry = tryResolve("@alienplatform/sdk", from); - if (sdkEntry) { nativeEntry = tryResolve("@alienplatform/bindings/native", path.dirname(sdkEntry)); route = "sdk"; } -} + if (sdkEntry) {{ nativeEntry = tryResolve(nativeSpec, path.dirname(sdkEntry)); route = "sdk"; }} +}} if (nativeEntry) process.stdout.write(route + "\n" + nativeEntry); -"#; +"# + ) +} -/// Resolve the `dist/` directory of `@alienplatform/bindings` as the app itself -/// resolves the package — directly, or transitively through `@alienplatform/sdk` -/// (the only path a Worker has). Returns `None` when the app depends on neither, -/// i.e. it is not a bindings consumer and staging is a no-op. +/// Resolve the `dist/` directory of `spec.package` as the app itself resolves it +/// — directly, or transitively through `@alienplatform/sdk` (the only path a +/// Worker has). Returns `None` when the app depends on neither, i.e. it is not a +/// consumer of that package and staging is a no-op. /// /// Resolution is delegated to bun (already required by the compile step) so pnpm /// symlinks and package `exports` are honored — the naive -/// `src_dir/node_modules/@alienplatform/bindings` path does not exist for a -/// Worker, whose bindings copy lives under the SDK's dependency. This function -/// is exercised by the SDK-entry compiled-artifact oracle -/// (`packages/package-layout/steps/compile.ts` covers the `/native` entry; -/// the Worker/SDK entry is covered by deployment E2E), not a hermetic unit -/// test, since faithful resolution requires bun and a real installed layout. -async fn resolve_bindings_dist_dir( +/// `src_dir/node_modules/` path does not exist for a Worker, whose copy +/// lives under the SDK's dependency. This function is exercised by the SDK-entry +/// compiled-artifact oracle (`packages/package-layout/steps/compile.ts` covers +/// the `/native` entry; the Worker/SDK entry is covered by deployment E2E), not +/// a hermetic unit test, since faithful resolution requires bun and a real +/// installed layout. +async fn resolve_addon_dist_dir( src_dir: &Path, + spec: &NativeAddonSpec, resource_name: &str, ) -> Result> { let output = Command::new("bun") - .args(["-e", RESOLVE_BINDINGS_NATIVE_SCRIPT]) - .env("ALIEN_BINDINGS_RESOLVE_FROM", src_dir) + .args(["-e", &resolve_native_script(spec.package)]) + .env("ALIEN_ADDON_RESOLVE_FROM", src_dir) .current_dir(src_dir) .output() .await .into_alien_error() .context(ErrorData::ImageBuildFailed { resource_name: resource_name.to_string(), - reason: "Failed to run bun to resolve @alienplatform/bindings. Is Bun installed?" - .to_string(), + reason: format!( + "Failed to run bun to resolve {}. Is Bun installed?", + spec.package + ), build_output: None, })?; @@ -241,7 +283,8 @@ async fn resolve_bindings_dist_dir( return Err(AlienError::new(ErrorData::ImageBuildFailed { resource_name: resource_name.to_string(), reason: format!( - "bun failed while resolving @alienplatform/bindings from {}", + "bun failed while resolving {} from {}", + spec.package, src_dir.display() ), build_output: Some(String::from_utf8_lossy(&output.stderr).into_owned()), @@ -250,7 +293,7 @@ async fn resolve_bindings_dist_dir( let resolved = String::from_utf8_lossy(&output.stdout).trim().to_string(); if resolved.is_empty() { - // App depends on neither the bindings package nor the SDK: no addon to embed. + // App depends on neither this package nor the SDK: no addon to embed. return Ok(None); } // First line is the resolution route ("direct" or "sdk"), second the @@ -259,7 +302,7 @@ async fn resolve_bindings_dist_dir( let (route_str, native_entry) = resolved.split_once('\n').ok_or_else(|| { AlienError::new(ErrorData::ImageBuildFailed { resource_name: resource_name.to_string(), - reason: format!("Unexpected output resolving @alienplatform/bindings: '{resolved}'"), + reason: format!("Unexpected output resolving {}: '{resolved}'", spec.package), build_output: None, }) })?; @@ -273,7 +316,8 @@ async fn resolve_bindings_dist_dir( return Err(AlienError::new(ErrorData::ImageBuildFailed { resource_name: resource_name.to_string(), reason: format!( - "Unexpected bindings resolution route '{other}' from the resolver script" + "Unexpected resolution route '{other}' for {} from the resolver script", + spec.package ), build_output: None, })); @@ -286,8 +330,8 @@ async fn resolve_bindings_dist_dir( AlienError::new(ErrorData::ImageBuildFailed { resource_name: resource_name.to_string(), reason: format!( - "Resolved bindings native entry '{}' has no parent directory", - native_entry + "Resolved {} native entry '{}' has no parent directory", + spec.package, native_entry ), build_output: None, }) @@ -316,7 +360,10 @@ pub(super) enum AddonResolutionRoute { /// for multi-target throughput. pub(super) struct StagedAddon { pub route: AddonResolutionRoute, - _guard: tokio::sync::OwnedMutexGuard<()>, + // One guard per staged addon (bindings, and ai-gateway when present). Each + // holds its per-dist-path lock from staging until the caller's compile + // finishes, so a concurrent build cannot swap a staged file mid-embed. + _guards: Vec>, } /// Workspace dev addon files that exist for the requested targets, walking @@ -324,26 +371,29 @@ pub(super) struct StagedAddon { /// package). Used by the build cache key: the compiled binary embeds these /// bytes, so a rebuilt addon must invalidate cached artifacts. pub(crate) fn workspace_addon_inputs(anchor: &Path, targets: &[BinaryTarget]) -> Vec { - let mut crate_dir: Option = None; - let mut dir = Some(anchor); - while let Some(current) = dir { - let candidate = current.join("crates").join("alien-bindings-node"); - if candidate.is_dir() { - crate_dir = Some(candidate); - break; + let mut inputs: Vec = Vec::new(); + for spec in [&BINDINGS, &AI_GATEWAY] { + let mut crate_dir: Option = None; + let mut dir = Some(anchor); + while let Some(current) = dir { + let candidate = current.join("crates").join(spec.crate_dir); + if candidate.is_dir() { + crate_dir = Some(candidate); + break; + } + dir = current.parent(); } - dir = current.parent(); + let Some(crate_dir) = crate_dir else { + continue; + }; + inputs.extend( + targets + .iter() + .filter_map(|target| napi_triple(*target)) + .map(|triple| crate_dir.join(format!("{}.{triple}.node", spec.crate_dir))) + .filter(|path| path.is_file()), + ); } - let Some(crate_dir) = crate_dir else { - return Vec::new(); - }; - - let mut inputs: Vec = targets - .iter() - .filter_map(|target| napi_triple(*target)) - .map(|triple| crate_dir.join(format!("alien-bindings-node.{triple}.node"))) - .filter(|path| path.is_file()) - .collect(); inputs.sort(); inputs.dedup(); inputs @@ -366,65 +416,103 @@ async fn lock_staged_path(staged: &Path) -> tokio::sync::OwnedMutexGuard<()> { lock.lock_owned().await } -/// Stage the TARGET platform's native addon next to the bindings package's -/// `dist/native.js` so `bun build --compile` can embed it. +/// Stage the TARGET platform's native addons next to each package's +/// `dist/native.js` so `bun build --compile` can embed them. /// -/// The `./native` entry of `@alienplatform/bindings` imports the addon through -/// the literal specifier `./alien-bindings.node` (see -/// `packages/bindings/src/native.ts`); this function fulfills that staging -/// contract. Returns the staged path (for post-build clean-up) plus how the -/// app resolves the bindings package (directly or via the SDK) when the app -/// consumes it, `None` when it does not. Fails with a clear error naming the -/// missing prebuild package when a consumer has no addon for the target — -/// otherwise `bun build --compile` would fail with an opaque unresolved-import -/// error. +/// Each package's `./native` entry imports its addon through a literal specifier +/// (`./alien-bindings.node`, `./alien-ai-gateway.node`); this fulfills that +/// staging contract for both. Bindings is required whenever the app resolves it — +/// a missing addon for the target fails with a clear error naming the prebuild, +/// since otherwise `bun build --compile` fails with an opaque unresolved-import. +/// The ai-gateway addon is staged best-effort: a Worker resolves it transitively +/// through the SDK even when it never calls `ai()`, so a missing addon there is +/// skipped, not a build error (requiring it would regress every non-AI Worker). +/// Returns the primary (bindings) resolution route — the generated compile entry +/// installs both via `@alienplatform/sdk/native` — plus the guards that keep the +/// staged files valid until the compile finishes. `None` when the app consumes +/// no native addons. pub(super) async fn stage_native_addon( src_dir: &Path, target: BinaryTarget, resource_name: &str, ) -> Result> { - let Some((bindings_dist, route)) = resolve_bindings_dist_dir(src_dir, resource_name).await? + // Bindings is the primary addon: its resolution route drives the generated + // entry, and an app that resolves neither it nor the SDK embeds nothing. + let Some((bindings_dist, route)) = + resolve_addon_dist_dir(src_dir, &BINDINGS, resource_name).await? else { return Ok(None); }; - // Take the per-path staging lock BEFORE writing, and hand it to the - // caller so it survives until the compile that embeds the staged file - // has finished (see `StagedAddon`). - let guard = lock_staged_path(&bindings_dist.join(STAGED_ADDON_FILE)).await; - stage_addon_into(src_dir, &bindings_dist, target, resource_name).await?; + let mut guards = Vec::new(); + // Take the per-path staging lock BEFORE writing, and hand it to the caller so + // it survives until the compile that embeds the staged file has finished. + guards.push(lock_staged_path(&bindings_dist.join(BINDINGS.staged_file)).await); + stage_addon_into(src_dir, &BINDINGS, &bindings_dist, target, resource_name, true).await?; + + // AI gateway: best-effort. Stage it only when the app resolves it AND an + // addon for the target exists; skipping keeps non-AI Workers building. + if let Some((ai_dist, _ai_route)) = + resolve_addon_dist_dir(src_dir, &AI_GATEWAY, resource_name).await? + { + let ai_guard = lock_staged_path(&ai_dist.join(AI_GATEWAY.staged_file)).await; + if stage_addon_into(src_dir, &AI_GATEWAY, &ai_dist, target, resource_name, false) + .await? + .is_some() + { + guards.push(ai_guard); + } + } + Ok(Some(StagedAddon { route, - _guard: guard, + _guards: guards, })) } -/// Source the target addon and copy it into `bindings_dist` as the staged -/// `alien-bindings.node`. Split from {@link stage_native_addon} so the sourcing -/// and copy logic is unit-testable against a fixture `dist/` directory without -/// invoking bun's resolver. +/// Source the target addon for `spec` and copy it into `addon_dist` as the +/// staged `spec.staged_file`. Split from {@link stage_native_addon} so the +/// sourcing and copy logic is unit-testable against a fixture `dist/` directory +/// without invoking bun's resolver. +/// +/// `required` distinguishes the two addons: bindings (`required = true`) errors +/// when no addon exists for the target; ai-gateway (`required = false`) returns +/// `Ok(None)` instead, so a Worker that resolves ai-gateway through the SDK but +/// has no addon for the target still builds (it just can't call `ai()` in the +/// compiled binary). Returns `Some(staged_path)` when it staged, `None` when it +/// skipped an optional addon. async fn stage_addon_into( src_dir: &Path, - bindings_dist: &Path, + spec: &NativeAddonSpec, + addon_dist: &Path, target: BinaryTarget, resource_name: &str, -) -> Result { + required: bool, +) -> Result> { let Some(triple) = napi_triple(target) else { + if !required { + info!( + "No {} native addon exists for build target '{}'; skipping (optional)", + spec.package, target + ); + return Ok(None); + } return Err(AlienError::new(ErrorData::ImageBuildFailed { resource_name: resource_name.to_string(), reason: format!( "{} is installed, but no native addon exists for build target '{}'. \ - Native bindings support linux-x64, linux-arm64, and darwin-arm64 targets.", - BINDINGS_PACKAGE, target + Native addons support linux-x64, linux-arm64, and darwin-arm64 targets.", + spec.package, target ), build_output: None, })); }; - let addon_file_name = format!("alien-bindings-node.{}.node", triple); + let addon_file_name = format!("{}.{}.node", spec.crate_dir, triple); let mut checked = Vec::new(); let Some(source) = find_addon_source( src_dir, - bindings_dist, + spec, + addon_dist, triple, &addon_file_name, resource_name, @@ -432,22 +520,34 @@ async fn stage_addon_into( ) .await? else { + if !required { + info!( + "Optional native addon {} for target '{}' not found; skipping (the app may not \ + use it). Checked: {}", + spec.package, + target, + checked.join(", ") + ); + return Ok(None); + } + let lib_name = format!("lib{}.so", spec.crate_dir.replace('-', "_")); return Err(AlienError::new(ErrorData::ImageBuildFailed { resource_name: resource_name.to_string(), reason: format!( "{pkg} is installed, but the native addon for target '{target}' was not found. \ Install the prebuild package '{pkg}-{triple}' (it ships {addon_file_name}), \ or, in the alien workspace, build the dev addon with \ - `npx napi build --platform --release` in crates/alien-bindings-node. \ + `npx napi build --platform --release` in crates/{crate_dir}. \ Cross-building from another OS: zig/napi-cross cannot build this cdylib; \ build natively in Docker instead: \ `docker run --rm --platform linux/ -v :/work \ - -e CARGO_TARGET_DIR=/tmp/target -w /work/crates/alien-bindings-node \ + -e CARGO_TARGET_DIR=/tmp/target -w /work/crates/{crate_dir} \ rust:1-bookworm sh -c 'apt-get update -qq && apt-get install -y -qq \ protobuf-compiler && cargo build --release --lib && \ - cp /tmp/target/release/libalien_bindings_node.so {addon_file_name}'`. \ + cp /tmp/target/release/{lib_name} {addon_file_name}'`. \ Checked: {checked}.", - pkg = BINDINGS_PACKAGE, + pkg = spec.package, + crate_dir = spec.crate_dir, checked = checked.join(", "), ), build_output: None, @@ -455,19 +555,15 @@ async fn stage_addon_into( }; // The staged path is a SHARED singleton (`native.js` imports the literal - // `./alien-bindings.node`), and concurrent builds — parallel containers in - // one stack, parallel tests — all stage into it. Write via a unique temp - // file + atomic rename so a concurrent `bun build --compile` never reads a - // half-written addon, and never delete it after a build (see the cleanup - // in `typescript.rs`): removing it would yank the file out from under a + // `./`), and concurrent builds — parallel containers in one + // stack, parallel tests — all stage into it. Write via a unique temp file + + // atomic rename so a concurrent `bun build --compile` never reads a + // half-written addon, and never delete it after a build (see the cleanup in + // `typescript.rs`): removing it would yank the file out from under a // concurrent compile. The dist directory is build output, so a lingering // copy is expected debris and the next staging simply renames over it. - let staged = bindings_dist.join(STAGED_ADDON_FILE); - let staged_tmp = bindings_dist.join(format!( - "{}.staging-{}", - STAGED_ADDON_FILE, - std::process::id() - )); + let staged = addon_dist.join(spec.staged_file); + let staged_tmp = addon_dist.join(format!("{}.staging-{}", spec.staged_file, std::process::id())); let stage_result = async { fs::copy(&source, &staged_tmp).await?; fs::rename(&staged_tmp, &staged).await @@ -489,12 +585,13 @@ async fn stage_addon_into( build_output: None, })?; info!( - "Staged native addon for {}: {} -> {}", + "Staged {} native addon for {}: {} -> {}", + spec.package, target, source.display(), staged.display() ); - Ok(staged) + Ok(Some(staged)) } #[cfg(test)] @@ -510,9 +607,15 @@ mod tests { /// bun resolution is needed; the resolver is verified by the compiled /// artifact oracle.) async fn install_fake_bindings_package(app_dir: &Path) -> PathBuf { + install_fake_addon_package(app_dir, &BINDINGS).await + } + + /// Create `/node_modules//dist/native.js` and return the + /// `dist/` path — the directory an addon is staged into. + async fn install_fake_addon_package(app_dir: &Path, spec: &NativeAddonSpec) -> PathBuf { let dist = app_dir .join("node_modules") - .join(BINDINGS_PACKAGE) + .join(spec.package) .join("dist"); fs::create_dir_all(&dist).await.unwrap(); fs::write(dist.join("native.js"), "// fake native entry") @@ -554,13 +657,21 @@ mod tests { .await .unwrap(); - let staged = stage_addon_into(app.path(), &bindings_dist, BinaryTarget::LinuxArm64, "app") - .await - .expect("staging should succeed from the installed prebuild"); + let staged = stage_addon_into( + app.path(), + &BINDINGS, + &bindings_dist, + BinaryTarget::LinuxArm64, + "app", + true, + ) + .await + .expect("staging should succeed from the installed prebuild") + .expect("required addon must stage"); assert_eq!( staged, - bindings_dist.join(STAGED_ADDON_FILE), + bindings_dist.join(BINDINGS.staged_file), "the addon must land next to dist/native.js under the exact name its static import uses" ); assert_eq!(fs::read(&staged).await.unwrap(), addon_bytes); @@ -593,9 +704,17 @@ mod tests { .await .unwrap(); - let staged = stage_addon_into(&app_dir, &bindings_dist, BinaryTarget::LinuxX64, "svc") - .await - .unwrap(); + let staged = stage_addon_into( + &app_dir, + &BINDINGS, + &bindings_dist, + BinaryTarget::LinuxX64, + "svc", + true, + ) + .await + .unwrap() + .expect("required addon must stage"); assert_eq!(fs::read(&staged).await.unwrap(), b"app-prebuild-addon"); } @@ -614,9 +733,17 @@ mod tests { let app_dir = root.path().join("apps").join("svc"); let bindings_dist = install_fake_bindings_package(&app_dir).await; - let staged = stage_addon_into(&app_dir, &bindings_dist, BinaryTarget::LinuxX64, "svc") - .await - .unwrap(); + let staged = stage_addon_into( + &app_dir, + &BINDINGS, + &bindings_dist, + BinaryTarget::LinuxX64, + "svc", + true, + ) + .await + .unwrap() + .expect("required addon must stage"); assert_eq!(fs::read(&staged).await.unwrap(), b"workspace-dev-addon"); } @@ -634,7 +761,7 @@ mod tests { }; let triple = napi_triple(target).unwrap(); - let error = stage_addon_into(app.path(), &bindings_dist, target, "app") + let error = stage_addon_into(app.path(), &BINDINGS, &bindings_dist, target, "app", true) .await .expect_err("staging must fail when no addon source exists"); let message = error.to_string(); @@ -649,12 +776,83 @@ mod tests { let app = tempdir().unwrap(); let bindings_dist = install_fake_bindings_package(app.path()).await; - let error = stage_addon_into(app.path(), &bindings_dist, BinaryTarget::WindowsX64, "app") - .await - .expect_err("windows has no native addon"); + let error = stage_addon_into( + app.path(), + &BINDINGS, + &bindings_dist, + BinaryTarget::WindowsX64, + "app", + true, + ) + .await + .expect_err("windows has no native addon"); assert!( error.to_string().contains("windows-x64"), "error should name the unsupported target, got: {error}" ); } + + /// The ai-gateway addon is staged best-effort: when its addon for the target + /// is missing, staging returns `Ok(None)` (skip) instead of failing — a + /// non-AI Worker that resolves ai-gateway through the SDK must still build. + #[tokio::test] + async fn optional_addon_skips_when_source_is_missing() { + let app = tempdir().unwrap(); + let ai_dist = install_fake_addon_package(app.path(), &AI_GATEWAY).await; + + // Cross target (never the host), no prebuild, no workspace crate above + // the temp dir — no ai-gateway addon source exists. + let target = if BinaryTarget::current_os() == BinaryTarget::LinuxArm64 { + BinaryTarget::LinuxX64 + } else { + BinaryTarget::LinuxArm64 + }; + + let staged = stage_addon_into(app.path(), &AI_GATEWAY, &ai_dist, target, "app", false) + .await + .expect("optional staging must not error when the addon is missing"); + assert!( + staged.is_none(), + "a missing optional addon must be skipped (None), not staged" + ); + assert!( + !ai_dist.join(AI_GATEWAY.staged_file).exists(), + "nothing should be staged when the optional addon is absent" + ); + } + + /// When the ai-gateway addon IS present it stages under its own literal file + /// name (`alien-ai-gateway.node`) next to its own dist/native.js. + #[tokio::test] + async fn optional_ai_gateway_addon_stages_when_present() { + let app = tempdir().unwrap(); + let ai_dist = install_fake_addon_package(app.path(), &AI_GATEWAY).await; + + let prebuild_dir = app + .path() + .join("node_modules") + .join("@alienplatform/ai-gateway-linux-arm64-gnu"); + fs::create_dir_all(&prebuild_dir).await.unwrap(); + let addon_bytes = b"fake-ai-gateway-linux-arm64-addon"; + fs::write( + prebuild_dir.join("alien-ai-gateway-node.linux-arm64-gnu.node"), + addon_bytes, + ) + .await + .unwrap(); + + let staged = stage_addon_into( + app.path(), + &AI_GATEWAY, + &ai_dist, + BinaryTarget::LinuxArm64, + "app", + false, + ) + .await + .expect("staging should succeed from the installed prebuild") + .expect("present optional addon must stage"); + assert_eq!(staged, ai_dist.join(AI_GATEWAY.staged_file)); + assert_eq!(fs::read(&staged).await.unwrap(), addon_bytes); + } } diff --git a/crates/alien-cli/src/commands/init.rs b/crates/alien-cli/src/commands/init.rs index 9301fcab6..e229d7766 100644 --- a/crates/alien-cli/src/commands/init.rs +++ b/crates/alien-cli/src/commands/init.rs @@ -62,6 +62,10 @@ const KNOWN_TEMPLATES: &[(&str, &str)] = &[ "nextjs-app", "Deploy a Next.js app as a single container in the customer's cloud.", ), + ( + "ai-quickstart-ts", + "The smallest AI setup: one worker calling cloud LLMs, no API keys, no database.", + ), ]; fn fallback_templates() -> Vec { diff --git a/crates/alien-cloudformation/src/built_ins.rs b/crates/alien-cloudformation/src/built_ins.rs index 61d6440ee..d71535b14 100644 --- a/crates/alien-cloudformation/src/built_ins.rs +++ b/crates/alien-cloudformation/src/built_ins.rs @@ -5,15 +5,16 @@ use crate::{ emitters::aws::{ - AwsArtifactRegistryEmitter, AwsBuildEmitter, AwsEmailEmitter, AwsKubernetesClusterEmitter, - AwsKvEmitter, AwsNetworkEmitter, AwsOpenSearchEmitter, AwsQueueEmitter, - AwsRemoteStackManagementEmitter, AwsServiceAccountEmitter, AwsStorageEmitter, - AwsVaultEmitter, AwsWorkerEmitter, + AwsAiEmitter, AwsArtifactRegistryEmitter, AwsBuildEmitter, AwsEmailEmitter, + AwsKubernetesClusterEmitter, AwsKvEmitter, AwsNetworkEmitter, AwsOpenSearchEmitter, + AwsQueueEmitter, AwsRemoteStackManagementEmitter, AwsServiceAccountEmitter, + AwsStorageEmitter, AwsVaultEmitter, AwsWorkerEmitter, }, registry::CfRegistry, }; use alien_core::{ - ArtifactRegistry, AwsOpenSearch, Build, Email, KubernetesCluster, Kv, Network, Platform, Queue, + Ai, ArtifactRegistry, AwsOpenSearch, Build, Email, KubernetesCluster, Kv, Network, Platform, + Queue, RemoteStackManagement, ResourceType, ServiceAccount, Storage, Vault, Worker, }; @@ -25,6 +26,7 @@ pub(crate) fn register_aws(registry: &mut CfRegistry) { registry.register(resource_type, Platform::Aws, emitter); } + aws(registry, Ai::RESOURCE_TYPE, AwsAiEmitter); aws(registry, Storage::RESOURCE_TYPE, AwsStorageEmitter); // Experimental resources are provider-specific: AwsOpenSearch only // registers an AWS emitter, so other platforms fail with a typed diff --git a/crates/alien-cloudformation/src/emitters/aws/ai.rs b/crates/alien-cloudformation/src/emitters/aws/ai.rs new file mode 100644 index 000000000..856e05c9e --- /dev/null +++ b/crates/alien-cloudformation/src/emitters/aws/ai.rs @@ -0,0 +1,266 @@ +//! AWS AI — Bedrock inference gateway. +//! +//! AWS Bedrock is a regional, account-scoped service with no per-stack +//! cloud resource to provision. The emitter returns zero CFN resources for +//! the AI resource itself and emits `AWS::IAM::Policy` resources for every +//! permission profile that references an `ai/*` set (e.g. `ai/invoke`, or +//! `ai/finetune` when the resource declares a fine-tuning job) on this +//! resource, attaching them to the corresponding service-account role. +//! +//! The import ref carries the region so the controller can reconstruct the +//! Bedrock endpoint without a cloud round-trip. + +use crate::{ + emitter::CfEmitter, + emitters::aws::{ + helpers::{ + cf_from_json, required_logical_id, resource_config, service_account_role_id, + service_trust_policy, stack_name, tags, uniquify_iam_statement_sids, + }, + service_account::permission_context, + }, + template::{CfExpression, CfResource}, +}; +use alien_core::{ + import::EmitContext, Ai, ErrorData, PermissionProfile, PermissionSetReference, Result, Storage, +}; +use alien_error::{AlienError, Context, IntoAlienError}; +use alien_permissions::{generators::AwsCloudFormationPermissionsGenerator, BindingTarget}; + +/// The `ai/finetune` permission set id. When a permission profile references it on +/// this AI resource, the emitter provisions a dedicated Bedrock-trusted IAM role so +/// Bedrock can assume it to read training data and write output. +const AI_FINETUNE_PERMISSION_ID: &str = "ai/finetune"; + +#[derive(Debug, Clone, Copy, Default)] +pub struct AwsAiEmitter; + +impl CfEmitter for AwsAiEmitter { + fn emit_resources(&self, ctx: &EmitContext<'_>) -> Result> { + let ai = resource_config::(ctx, Ai::RESOURCE_TYPE)?; + let mut resources = ai_iam_policies(ctx)?; + + // When a permission profile references `ai/finetune` on this resource, emit a + // dedicated IAM role Bedrock can assume for model-customization jobs. The + // stack's service-account roles only trust compute principals + // (`lambda`/`codebuild`/`ec2`), so Bedrock cannot assume them — that is the + // real AccessDenied this role fixes. Its name matches the controller's + // `role_arn` (`{prefix}-{id}-finetune`) so the runtime gateway can pass it. + if resource_references_finetune(ctx) { + resources.push(finetune_role(ctx, ai.id())); + } + + Ok(resources) + } + + fn emit_import_ref(&self, ctx: &EmitContext<'_>) -> Result { + resource_config::(ctx, Ai::RESOURCE_TYPE)?; + Ok(CfExpression::object([("region", CfExpression::ref_("AWS::Region"))])) + } +} + +fn ai_iam_policies(ctx: &EmitContext<'_>) -> Result> { + let mut resources = Vec::new(); + let generator = AwsCloudFormationPermissionsGenerator::new(); + let logical_id = required_logical_id(ctx)?; + let context = permission_context(); + + for (owner_index, (role_id, permission_refs)) in ai_permission_owners(ctx).into_iter().enumerate() { + for (permission_index, permission_ref) in permission_refs.iter().enumerate() { + let Some(permission_set) = + permission_ref.resolve(|name| alien_permissions::get_permission_set(name).cloned()) + else { + continue; + }; + + let policy = generator + .generate_policy(&permission_set, BindingTarget::Resource, &context) + .context(ErrorData::GenericError { + message: format!( + "failed to generate AWS CloudFormation AI IAM policy for '{}'", + ctx.resource_id + ), + })?; + let policy_value = serde_json::to_value(policy).into_alien_error().context( + ErrorData::TemplateSerializationFailed { + format: "CloudFormation IAM policy".to_string(), + reason: "Failed to serialize AI IAM policy".to_string(), + }, + )?; + let CfExpression::Object(mut policy_object) = cf_from_json(policy_value)? else { + return Err(AlienError::new(ErrorData::TemplateSerializationFailed { + format: "CloudFormation IAM policy".to_string(), + reason: "AI policy did not serialize to a JSON object".to_string(), + })); + }; + let Some(CfExpression::List(policy_statements)) = + policy_object.shift_remove("Statement") + else { + continue; + }; + + let policy_id = format!( + "{logical_id}{role_id}AiPermission{owner_index}{permission_index}" + ); + let mut policy_resource = + CfResource::new(policy_id, "AWS::IAM::Policy".to_string()); + policy_resource.properties.insert( + "PolicyName".to_string(), + CfExpression::sub(format!( + "${{AWS::StackName}}-{}-ai-{owner_index}-{permission_index}", + ctx.resource_id + )), + ); + policy_resource.properties.insert( + "PolicyDocument".to_string(), + CfExpression::object([ + ("Version", CfExpression::from("2012-10-17")), + ( + "Statement", + CfExpression::list(uniquify_iam_statement_sids(policy_statements)), + ), + ]), + ); + policy_resource.properties.insert( + "Roles".to_string(), + CfExpression::list([CfExpression::ref_(&role_id)]), + ); + policy_resource.depends_on.push(role_id.clone()); + resources.push(policy_resource); + } + } + + Ok(resources) +} + +fn ai_permission_owners(ctx: &EmitContext<'_>) -> Vec<(String, Vec)> { + let mut owners = Vec::new(); + for (profile_name, profile) in ctx.stack.permission_profiles() { + let refs = ai_permission_refs(profile, ctx.resource_id); + if refs.is_empty() { + continue; + } + if let Some(role_id) = service_account_role_id(ctx, profile_name) { + owners.push((role_id, refs)); + } + } + owners +} + +fn ai_permission_refs( + profile: &PermissionProfile, + resource_id: &str, +) -> Vec { + let mut refs = Vec::new(); + let mut seen_ids = std::collections::HashSet::new(); + if let Some(resource_refs) = profile.0.get(resource_id) { + for permission_ref in resource_refs { + if seen_ids.insert(permission_ref.id().to_string()) { + refs.push(permission_ref.clone()); + } + } + } + refs +} + +/// True when any permission profile references the `ai/finetune` set on this AI +/// resource. Only then is the Bedrock-trusted finetune role needed. +fn resource_references_finetune(ctx: &EmitContext<'_>) -> bool { + ctx.stack.permission_profiles().values().any(|profile| { + ai_permission_refs(profile, ctx.resource_id) + .iter() + .any(|reference| reference.id() == AI_FINETUNE_PERMISSION_ID) + }) +} + +/// The dedicated Bedrock-trusted finetune IAM role plus its inline S3 policy. +/// +/// The role is named `${AWS::StackName}-{id}-finetune` (matching the controller's +/// `role_arn`), trusts `bedrock.amazonaws.com`, and can read training data +/// (`s3:GetObject`/`s3:ListBucket`) and write output (`s3:PutObject`) on every +/// storage bucket in the stack. +fn finetune_role(ctx: &EmitContext<'_>, ai_id: &str) -> CfResource { + let logical_id = ctx.name_for(ctx.resource_id).unwrap_or(ctx.resource_id); + let role_id = format!("{logical_id}FinetuneRole"); + + let mut role = CfResource::new(role_id, "AWS::IAM::Role".to_string()); + role.properties + .insert("RoleName".to_string(), stack_name(&format!("{ai_id}-finetune"))); + role.properties.insert( + "AssumeRolePolicyDocument".to_string(), + service_trust_policy(["bedrock.amazonaws.com"]), + ); + role.properties.insert( + "Policies".to_string(), + CfExpression::list([CfExpression::object([ + ("PolicyName", stack_name(&format!("{ai_id}-finetune-s3"))), + ( + "PolicyDocument", + CfExpression::object([ + ("Version", CfExpression::from("2012-10-17")), + ("Statement", CfExpression::list(finetune_s3_statements(ctx))), + ]), + ), + ])]), + ); + role.properties.insert("Tags".to_string(), tags(ctx)); + + // The role reads/writes the storage buckets, so it must be created after them. + for bucket_id in storage_bucket_logical_ids(ctx) { + role.depends_on.push(bucket_id); + } + + role +} + +/// S3 statements scoping the finetune role to the stack's storage buckets: +/// list/read on the bucket + objects, put on objects (training in, output out). +fn finetune_s3_statements(ctx: &EmitContext<'_>) -> Vec { + let mut bucket_arns = Vec::new(); + let mut object_arns = Vec::new(); + for bucket_id in storage_bucket_logical_ids(ctx) { + bucket_arns.push(CfExpression::get_att(&bucket_id, "Arn")); + object_arns.push(CfExpression::sub(format!("${{{bucket_id}.Arn}}/*"))); + } + + vec![ + // Read the training dataset: list the bucket and get objects. + CfExpression::object([ + ("Effect", CfExpression::from("Allow")), + ( + "Action", + CfExpression::list([ + CfExpression::from("s3:GetObject"), + CfExpression::from("s3:ListBucket"), + ]), + ), + ( + "Resource", + CfExpression::list( + bucket_arns + .iter() + .cloned() + .chain(object_arns.iter().cloned()), + ), + ), + ]), + // Write the tuning-job output back to storage. + CfExpression::object([ + ("Effect", CfExpression::from("Allow")), + ("Action", CfExpression::from("s3:PutObject")), + ("Resource", CfExpression::list(object_arns)), + ]), + ] +} + +/// Logical ids of every `Storage` (S3 bucket) resource in the stack. +fn storage_bucket_logical_ids(ctx: &EmitContext<'_>) -> Vec { + ctx.stack + .resources() + .filter_map(|(id, entry)| { + entry.config.downcast_ref::()?; + ctx.name_for(id).map(|label| label.to_string()) + }) + .collect() +} + diff --git a/crates/alien-cloudformation/src/emitters/aws/mod.rs b/crates/alien-cloudformation/src/emitters/aws/mod.rs index 777e90040..2dcade453 100644 --- a/crates/alien-cloudformation/src/emitters/aws/mod.rs +++ b/crates/alien-cloudformation/src/emitters/aws/mod.rs @@ -5,6 +5,7 @@ //! Built-ins are wired through [`crate::CfRegistry::built_in`]; plugins //! register additional implementations against the same registry. +pub mod ai; pub mod artifact_registry; pub mod build; pub mod email; @@ -20,6 +21,7 @@ pub mod storage; pub mod vault; pub mod worker; +pub use ai::AwsAiEmitter; pub use artifact_registry::AwsArtifactRegistryEmitter; pub use build::AwsBuildEmitter; pub use email::AwsEmailEmitter; diff --git a/crates/alien-cloudformation/tests/generator.rs b/crates/alien-cloudformation/tests/generator.rs index f3899a2de..6270e7b05 100644 --- a/crates/alien-cloudformation/tests/generator.rs +++ b/crates/alien-cloudformation/tests/generator.rs @@ -10,6 +10,7 @@ mod generator { pub mod helpers; + pub mod aws_ai_tests; pub mod aws_compute_tests; pub mod aws_data_layer_tests; pub mod aws_email_tests; diff --git a/crates/alien-cloudformation/tests/generator/aws_ai_tests.rs b/crates/alien-cloudformation/tests/generator/aws_ai_tests.rs new file mode 100644 index 000000000..f66b8ac67 --- /dev/null +++ b/crates/alien-cloudformation/tests/generator/aws_ai_tests.rs @@ -0,0 +1,155 @@ +//! AWS AI (Bedrock) CloudFormation emitter tests. + +use super::helpers::render_built_ins; +use alien_cloudformation::RegistrationMode; +use alien_core::{ + Ai, PermissionProfile, ResourceLifecycle, ServiceAccount, Stack, StackSettings, Storage, +}; + +#[test] +fn aws_ai_invoke_permissions_attach_to_service_account_role() { + let stack = Stack::new("ai-permissions".to_string()) + .permission( + "execution", + PermissionProfile::new().resource("llm", ["ai/invoke"]), + ) + .add( + ServiceAccount::new("execution-sa".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + + let yaml = render_built_ins( + &stack, + StackSettings::default(), + RegistrationMode::OutputsFallback, + "aws ai bedrock invoke permissions", + ); + + // Policy resource is emitted and attached to the execution service-account role. + assert!( + yaml.contains("ExecutionSaRole"), + "expected ExecutionSaRole in template:\n{yaml}" + ); + // Bedrock actions are present. + assert!( + yaml.contains("bedrock:InvokeModel"), + "expected bedrock:InvokeModel in template:\n{yaml}" + ); + assert!( + yaml.contains("bedrock:InvokeModelWithResponseStream"), + "expected bedrock:InvokeModelWithResponseStream in template:\n{yaml}" + ); + // Bedrock foundation-model resource scope is present. + assert!( + yaml.contains("foundation-model"), + "expected arn:aws:bedrock:*::foundation-model/* in template:\n{yaml}" + ); + + // Snapshot the full rendered template so the IAM::Policy structure — role ref, + // DependsOn, logical-id uniqueness, SID shape — is proven at the artifact level, + // matching the terraform path (render_built_ins already runs cfn-lint on it). + insta::assert_snapshot!("aws_ai_invoke_permissions", yaml); +} + +#[test] +fn aws_ai_without_permissions_emits_no_iam_policy() { + // An Ai resource with no permission profile referencing it should emit + // zero IAM resources (the AI itself creates no cloud resource on AWS). + let stack = Stack::new("ai-no-permissions".to_string()) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + + let yaml = render_built_ins( + &stack, + StackSettings::default(), + RegistrationMode::OutputsFallback, + "aws ai no permissions", + ); + + assert!( + !yaml.contains("bedrock:InvokeModel"), + "expected no bedrock actions without a permission profile:\n{yaml}" + ); + assert!( + !yaml.contains("AWS::IAM::Policy"), + "expected no IAM policy without a permission profile:\n{yaml}" + ); + // A pure inference gateway must NOT get a Bedrock-trusted finetune role. + assert!( + !yaml.contains("bedrock.amazonaws.com"), + "expected no bedrock trust policy without ai/finetune:\n{yaml}" + ); +} + +#[test] +fn aws_ai_finetune_emits_bedrock_trusted_role_with_s3_policy() { + // When a permission profile references ai/finetune on the AI resource, the + // emitter provisions a dedicated IAM role Bedrock can assume (the real fix for + // AccessDenied: service-account roles only trust compute principals) with an + // inline S3 policy over the stack's storage buckets. + let stack = Stack::new("ai-finetune".to_string()) + .permission( + "execution", + PermissionProfile::new().resource("llm", ["ai/finetune"]), + ) + .add( + ServiceAccount::new("execution-sa".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add( + Storage::new("training".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add( + Ai::new("llm".to_string()) + .finetune(alien_core::FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: "training".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: alien_core::FinetuneMethod::Sft, + }) + .build(), + ResourceLifecycle::Frozen, + ) + .build(); + + let yaml = render_built_ins( + &stack, + StackSettings::default(), + RegistrationMode::OutputsFallback, + "aws ai bedrock finetune role", + ); + + // The dedicated finetune role exists and is named deterministically to match + // the controller's role_arn (`{prefix}-{id}-finetune`). + assert!( + yaml.contains("AWS::IAM::Role"), + "expected a dedicated finetune IAM::Role:\n{yaml}" + ); + assert!( + yaml.contains("${AWS::StackName}-llm-finetune"), + "expected role name ${{prefix}}-llm-finetune matching the controller role_arn:\n{yaml}" + ); + // Its trust policy allows Bedrock to assume it — the crux of the fix. + assert!( + yaml.contains("bedrock.amazonaws.com"), + "finetune role must trust bedrock.amazonaws.com:\n{yaml}" + ); + // The inline policy grants S3 read (training data) and write (output). + assert!( + yaml.contains("s3:GetObject") && yaml.contains("s3:ListBucket"), + "finetune role must read the training dataset from S3:\n{yaml}" + ); + assert!( + yaml.contains("s3:PutObject"), + "finetune role must write tuning output to S3:\n{yaml}" + ); + // The S3 grants are scoped to the stack's storage bucket, not "*". + assert!( + yaml.contains("Training") || yaml.contains("training"), + "S3 grants must reference the storage bucket by ARN:\n{yaml}" + ); +} diff --git a/crates/alien-cloudformation/tests/generator/snapshots/generator__generator__aws_ai_tests__aws_ai_invoke_permissions.snap b/crates/alien-cloudformation/tests/generator/snapshots/generator__generator__aws_ai_tests__aws_ai_invoke_permissions.snap new file mode 100644 index 000000000..b41ba520e --- /dev/null +++ b/crates/alien-cloudformation/tests/generator/snapshots/generator__generator__aws_ai_tests__aws_ai_invoke_permissions.snap @@ -0,0 +1,191 @@ +--- +source: crates/alien-cloudformation/tests/generator/aws_ai_tests.rs +expression: yaml +--- +AWSTemplateFormatVersion: 2010-09-09 +Description: aws ai bedrock invoke permissions +Transform: +- AWS::LanguageExtensions +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: Registration + Parameters: + - Token + - ManagingRoleArn + - ManagingAccountId + - Label: + default: Operations + Parameters: + - UpdatesMode + - TelemetryMode + - HeartbeatsMode + ParameterLabels: + HeartbeatsMode: + default: Heartbeats + ManagingAccountId: + default: Image account ID + ManagingRoleArn: + default: Management role ARN + TelemetryMode: + default: Telemetry + Token: + default: Install token + UpdatesMode: + default: Updates +Parameters: + Token: + Type: String + Description: Install token from the application setup page. + NoEcho: true + ManagingRoleArn: + Type: String + Description: ARN of the management identity allowed to assume setup-created roles. + Default: '' + ManagingAccountId: + Type: String + Description: AWS account ID for the management account that hosts application container images. + Default: '' + NetworkMode: + Type: String + Description: Choose create-new for a managed VPC, use-existing for your VPC, or use-default for the account default VPC. + Default: create-new + AllowedValues: + - create-new + - use-existing + - use-default + UpdatesMode: + Type: String + Description: How updates are applied after setup registration. + Default: auto + AllowedValues: + - auto + - approval-required + TelemetryMode: + Type: String + Description: Telemetry collection behavior. + Default: auto + AllowedValues: + - auto + HeartbeatsMode: + Type: String + Description: Heartbeat health-check behavior. + Default: "on" + AllowedValues: + - "off" + - "on" +Resources: + ExecutionSaRole: + Type: AWS::IAM::Role + Properties: + RoleName: + Fn::Sub: ${AWS::StackName}-execution-sa + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - codebuild.amazonaws.com + - ec2.amazonaws.com + - lambda.amazonaws.com + Action: sts:AssumeRole + Tags: + - Key: managed-by + Value: setup + - Key: deployment + Value: + Ref: AWS::StackName + - Key: resource + Value: execution-sa + - Key: resource-type + Value: service-account + LlmExecutionSaRoleAiPermission00: + Type: AWS::IAM::Policy + Properties: + PolicyName: + Fn::Sub: ${AWS::StackName}-llm-ai-0-0 + PolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - bedrock-mantle:CreateInference + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Effect: Allow + Resource: + - Fn::Sub: arn:${AWS::Partition}:bedrock-mantle:*:${AWS::AccountId}:project/default + - Fn::Sub: arn:${AWS::Partition}:bedrock:*:${AWS::AccountId}:inference-profile/* + - Fn::Sub: arn:${AWS::Partition}:bedrock:*::foundation-model/* + Sid: AiInvoke + Roles: + - Ref: ExecutionSaRole + DependsOn: + - ExecutionSaRole +Outputs: + DeploymentSourceKind: + Value: cloudformation + Description: Setup source kind. + DeploymentResourcePrefix: + Value: + Ref: AWS::StackName + Description: Stable physical resource prefix. + DeploymentPlatform: + Value: aws + Description: Target platform. + DeploymentRegion: + Value: + Ref: AWS::Region + Description: AWS region. + DeploymentSetupTarget: + Value: aws + Description: Setup target. + DeploymentSetupFingerprint: + Value: test + Description: Setup compatibility fingerprint. + DeploymentSetupImportFormatVersion: + Value: 1 + Description: Setup registration payload format version. + DeploymentSetupFingerprintVersion: + Value: 1 + Description: Setup fingerprint algorithm version. + DeploymentManagementConfig: + Value: + Fn::ToJsonString: + platform: aws + managingRoleArn: + Ref: ManagingRoleArn + Description: Deployment registration management configuration JSON. + DeploymentStackSettings: + Value: + Fn::ToJsonString: + deploymentModel: push + updates: + Ref: UpdatesMode + telemetry: + Ref: TelemetryMode + heartbeats: + Ref: HeartbeatsMode + network: + Ref: AWS::NoValue + Description: Deployment registration settings JSON. + DeploymentResources: + Value: + Fn::ToJsonString: + - id: execution-sa + type: service-account + importData: + roleName: + Ref: ExecutionSaRole + roleArn: + Fn::GetAtt: + - ExecutionSaRole + - Arn + stackPermissionsApplied: true + - id: llm + type: ai + importData: + region: + Ref: AWS::Region + Description: Deployment registration resources JSON. diff --git a/crates/alien-core/src/ai_catalog.rs b/crates/alien-core/src/ai_catalog.rs new file mode 100644 index 000000000..42f983616 --- /dev/null +++ b/crates/alien-core/src/ai_catalog.rs @@ -0,0 +1,381 @@ +//! Curated, per-cloud model catalog for the AI gateway. +//! +//! Single source of truth for which public model ids each cloud exposes, the +//! upstream id the gateway forwards, and the wire protocol of the model's native +//! endpoint. Backs `getAvailableModels()` and the gateway's `/v1/models`, and the +//! Azure controller deploys the Azure entries as named deployments at provision +//! time (see `azure_deployments`). +//! +//! A model is includable only if its cloud serves it over a protocol the client +//! SDK already speaks (OpenAI Chat Completions or Anthropic Messages), so the +//! gateway forwards the request body untranslated. + +use crate::Platform; +use serde::{Deserialize, Serialize}; + +/// The upstream wire protocol a model speaks. The gateway forwards to the +/// matching native endpoint; the client SDK is responsible for speaking it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Protocol { + /// OpenAI Chat Completions (`/v1/chat/completions`). + OpenAi, + /// Anthropic Messages (`/v1/messages`). + Anthropic, +} + +/// One curated model: the public id an app requests, the cloud that serves it, +/// the upstream id the gateway forwards (for Azure this is the deployment name), +/// and the protocol of its native endpoint. +#[derive(Debug, Clone)] +pub struct CatalogModel { + pub public_id: &'static str, + pub cloud: Platform, + pub upstream_id: &'static str, + pub protocol: Protocol, +} + +static CATALOG: &[CatalogModel] = &[ + // AWS Bedrock over `/openai/v1` chat completions. The plain Bedrock model id, + // not the `us.*` cross-region inference profile — that endpoint rejects it. + // Invoke/Converse-only models (older Llama/Mistral-v0/Nova) can't be served here. + CatalogModel { public_id: "gpt-oss-20b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-20b-1:0", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gpt-oss-120b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-120b-1:0", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gpt-oss-safeguard-20b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-safeguard-20b", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gpt-oss-safeguard-120b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-safeguard-120b", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "deepseek-v3.2", cloud: Platform::Aws, upstream_id: "deepseek.v3.2", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "qwen3-32b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-32b-v1:0", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "qwen3-coder-30b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-coder-30b-a3b-v1:0", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "qwen3-coder-next", cloud: Platform::Aws, upstream_id: "qwen.qwen3-coder-next", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "qwen3-next-80b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-next-80b-a3b", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "qwen3-vl-235b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-vl-235b-a22b", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "mistral-large-3", cloud: Platform::Aws, upstream_id: "mistral.mistral-large-3-675b-instruct", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "devstral-2", cloud: Platform::Aws, upstream_id: "mistral.devstral-2-123b", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "magistral-small", cloud: Platform::Aws, upstream_id: "mistral.magistral-small-2509", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "ministral-3-14b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-14b-instruct", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "ministral-3-8b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-8b-instruct", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "ministral-3-3b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-3b-instruct", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "minimax-m2", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "minimax-m2.1", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2.1", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "minimax-m2.5", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2.5", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "kimi-k2.5", cloud: Platform::Aws, upstream_id: "moonshotai.kimi-k2.5", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "nemotron-nano-9b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-9b-v2", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "nemotron-nano-12b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-12b-v2", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "nemotron-nano-3-30b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-3-30b", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "nemotron-super-3-120b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-super-3-120b", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gemma-3-4b", cloud: Platform::Aws, upstream_id: "google.gemma-3-4b-it", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gemma-3-12b", cloud: Platform::Aws, upstream_id: "google.gemma-3-12b-it", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gemma-3-27b", cloud: Platform::Aws, upstream_id: "google.gemma-3-27b-it", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "glm-4.7", cloud: Platform::Aws, upstream_id: "zai.glm-4.7", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "glm-4.7-flash", cloud: Platform::Aws, upstream_id: "zai.glm-4.7-flash", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "glm-5", cloud: Platform::Aws, upstream_id: "zai.glm-5", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "palmyra-vision-7b", cloud: Platform::Aws, upstream_id: "writer.palmyra-vision-7b", protocol: Protocol::OpenAi }, + // AWS Bedrock, Claude over classic InvokeModel (the Anthropic Messages body is + // the InvokeModel body; the model travels in the URL). `upstream_id` is the plain + // Bedrock model id; the gateway prepends the region's cross-region inference-profile + // geo prefix (`us.`/`eu.`/`apac.`) at request time, since Claude is invocable only + // through a profile. Dated ids (`…--v1:0`) are required where AWS has no short + // alias. These need Claude model access granted on the deployment's account. + CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-5", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-8", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-7", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-6-v1", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-5-20251101-v1:0", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.1", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-1-20250805-v1:0", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-4-6", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-4-5-20250929-v1:0", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-haiku-4-5-20251001-v1:0", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-fable-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-fable-5", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-mythos-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-mythos-5", protocol: Protocol::Anthropic }, + // GCP Vertex, Gemini. The OpenAI-compatible Vertex endpoint expects the `google/` prefix. + // The 2.5 family serves in-region; the 3.x models serve on the `global` location. + CatalogModel { public_id: "gemini-2.5-pro", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-pro", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gemini-2.5-flash", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-flash", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gemini-2.5-flash-lite", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-flash-lite", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gemini-3.5-flash", cloud: Platform::Gcp, upstream_id: "google/gemini-3.5-flash", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gemini-3.1-flash-lite", cloud: Platform::Gcp, upstream_id: "google/gemini-3.1-flash-lite", protocol: Protocol::OpenAi }, + // GCP Vertex, Claude. The upstream id is the Vertex Model Garden id that travels + // in the `:rawPredict` URL path (`publishers/anthropic/models/`); models past + // Sonnet 4.5 carry no date suffix, older ones keep an `@` version. Needs + // Claude model access granted on the deployment's project. + CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Gcp, upstream_id: "claude-sonnet-5", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Gcp, upstream_id: "claude-opus-4-8", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Gcp, upstream_id: "claude-opus-4-7", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Gcp, upstream_id: "claude-opus-4-6", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Gcp, upstream_id: "claude-opus-4-5@20251101", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Gcp, upstream_id: "claude-sonnet-4-6", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Gcp, upstream_id: "claude-sonnet-4-5@20250929", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Gcp, upstream_id: "claude-haiku-4-5@20251001", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-fable-5", cloud: Platform::Gcp, upstream_id: "claude-fable-5", protocol: Protocol::Anthropic }, + // Azure, OpenAI-protocol. The upstream id is the deployment name the controller + // creates (see AZURE_DEPLOYMENTS); the app requests it by the same id. Azure serves + // only what is deployed, so this list must stay in sync with AZURE_DEPLOYMENTS. + CatalogModel { public_id: "gpt-4.1", cloud: Platform::Azure, upstream_id: "gpt-4.1", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "gpt-4o-mini", cloud: Platform::Azure, upstream_id: "gpt-4o-mini", protocol: Protocol::OpenAi }, + CatalogModel { public_id: "model-router", cloud: Platform::Azure, upstream_id: "model-router", protocol: Protocol::OpenAi }, + // Azure, Claude over the Foundry Anthropic endpoint. The upstream id is the + // Foundry deployment name (defaults to the model id). Unlike the OpenAI list, + // these are not in AZURE_DEPLOYMENTS: a first Claude deployment requires + // accepting Azure Marketplace terms, a portal step the controller cannot + // perform, so Claude deployments are created in the Foundry portal. Until + // that portal step runs, /v1/models advertises these while Foundry answers + // "deployment not found" — a deliberate tradeoff so the deployment-name + // contract is discoverable; the upstream 404 passes through attributably. + CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Azure, upstream_id: "claude-sonnet-5", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Azure, upstream_id: "claude-opus-4-8", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Azure, upstream_id: "claude-opus-4-7", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Azure, upstream_id: "claude-opus-4-6", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Azure, upstream_id: "claude-opus-4-5", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Azure, upstream_id: "claude-sonnet-4-6", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Azure, upstream_id: "claude-sonnet-4-5", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Azure, upstream_id: "claude-haiku-4-5", protocol: Protocol::Anthropic }, + CatalogModel { public_id: "claude-fable-5", cloud: Platform::Azure, upstream_id: "claude-fable-5", protocol: Protocol::Anthropic }, +]; + +/// Azure deployments to create at provision time: (deployment name, model name, +/// model version). The deployment name is the catalog `upstream_id`. The version +/// is validated against the target region's model catalog at deploy time. +static AZURE_DEPLOYMENTS: &[(&str, &str, &str)] = &[ + ("gpt-4.1", "gpt-4.1", "2025-04-14"), + ("gpt-4o-mini", "gpt-4o-mini", "2024-07-18"), + ("model-router", "model-router", "2025-11-18"), +]; + +/// AWS models servable over the bedrock-mantle OpenAI Responses API, mapped to the +/// id that endpoint expects (mantle drops the InvokeModel version suffix, and only +/// a subset of the chat catalog supports Responses at all — Claude is Messages-only +/// and e.g. Qwen rejects it). Kept explicit rather than derived: the two id schemes +/// differ per model family, not by a rule. +static RESPONSES_UPSTREAM: &[(&str, &str)] = &[ + ("gpt-oss-20b", "openai.gpt-oss-20b"), + ("gpt-oss-120b", "openai.gpt-oss-120b"), +]; + +/// The bedrock-mantle Responses-API id for a public model id, or `None` when the +/// model is not servable over the Responses API. +pub fn responses_upstream_id(public_id: &str) -> Option<&'static str> { + RESPONSES_UPSTREAM + .iter() + .find(|(public, _)| *public == public_id) + .map(|(_, upstream)| *upstream) +} + +pub fn models_for(cloud: Platform) -> Vec<&'static CatalogModel> { + CATALOG.iter().filter(|m| m.cloud == cloud).collect() +} + +/// The catalog model for a public id, or `None` if it is not exposed. +/// +/// First match: for an id serving on more than one cloud this is the AWS entry; +/// cloud-scoped callers use `lookup_for` via `resolve_for`. +pub fn lookup(public_id: &str) -> Option<&'static CatalogModel> { + CATALOG.iter().find(|m| m.public_id == public_id) +} + +fn lookup_for(public_id: &str, cloud: Platform) -> Option<&'static CatalogModel> { + CATALOG.iter().find(|m| m.public_id == public_id && m.cloud == cloud) +} + +/// The catalog model for a client-sent model id on a specific cloud. A public id +/// can appear once per cloud (Claude serves on more than one), so resolution must +/// scope to the binding's cloud rather than filter a first-match lookup — the +/// first match is another cloud's entry whenever ids overlap. +pub fn resolve_for(model_id: &str, cloud: Platform) -> Option<&'static CatalogModel> { + lookup_for(model_id, cloud).or_else(|| lookup_for(&canonical_public_id(model_id), cloud)) +} + +/// The catalog model for a client-sent model id, accepting the Anthropic-native +/// spellings agent CLIs actually send alongside the catalog's public ids. +/// +/// Claude Code's `/model` emits ids like `claude-sonnet-4-5-20250929` or +/// `claude-haiku-4-5`, Bedrock-aware clients may carry the full upstream id +/// (`us.anthropic.claude-haiku-4-5-20251001-v1:0`), and Vertex clients the +/// `@date` form (`claude-sonnet-4-5@20250929`). Exact public ids win; otherwise +/// the id is canonicalized — vendor/geo prefix, InvokeModel `-vN[:M]` suffix, +/// and either release-date suffix drop off, and a dashed minor version becomes +/// the catalog's dotted form (`claude-haiku-4-5` → `claude-haiku-4.5`). +/// +/// A public id can appear once per cloud, and this returns the first catalog +/// entry — for a multi-cloud id that is the AWS one. Callers routing by a +/// binding must use `resolve_for` with the binding's cloud. +pub fn resolve(model_id: &str) -> Option<&'static CatalogModel> { + lookup(model_id).or_else(|| lookup(&canonical_public_id(model_id))) +} + +fn canonical_public_id(model_id: &str) -> String { + let mut id = model_id; + if let Some(pos) = id.rfind("anthropic.") { + id = &id[pos + "anthropic.".len()..]; + } + // Vertex spells the release date as an `@` suffix rather than a dash. + id = id.split_once('@').map_or(id, |(base, _)| base); + id = strip_invoke_version(id); + id = strip_release_date(id); + dot_minor_version(id) +} + +/// Strip an InvokeModel version suffix: `-v1:0` or `-v1`. +fn strip_invoke_version(id: &str) -> &str { + let base = id.split_once(':').map_or(id, |(base, _)| base); + match base.rsplit_once("-v") { + Some((stem, digits)) if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) => { + stem + } + _ => base, + } +} + +/// Strip a release-date suffix: `-20251001`. +fn strip_release_date(id: &str) -> &str { + match id.rsplit_once('-') { + Some((stem, date)) + if date.len() == 8 && date.starts_with("20") && date.bytes().all(|b| b.is_ascii_digit()) => + { + stem + } + _ => id, + } +} + +/// Rewrite a trailing dashed minor version to the catalog's dotted form: +/// `claude-haiku-4-5` → `claude-haiku-4.5`. Whole versions (`claude-sonnet-5`) +/// are already in catalog form and pass through. +fn dot_minor_version(id: &str) -> String { + let Some((stem, minor)) = id.rsplit_once('-') else { + return id.to_string(); + }; + let Some((prefix, major)) = stem.rsplit_once('-') else { + return id.to_string(); + }; + let both_numeric = !major.is_empty() + && !minor.is_empty() + && major.bytes().all(|b| b.is_ascii_digit()) + && minor.bytes().all(|b| b.is_ascii_digit()); + if both_numeric { + format!("{prefix}-{major}.{minor}") + } else { + id.to_string() + } +} + +/// The Azure predefined model deployments, as (deployment name, model name, version). +pub fn azure_deployments() -> Vec<(&'static str, &'static str, &'static str)> { + AZURE_DEPLOYMENTS.to_vec() +} + +#[cfg(test)] +mod tests { + /// A public id may serve on more than one cloud (Claude does), but must appear at + /// most once per cloud — a duplicate within a cloud would make `resolve_for` + /// silently pick whichever entry comes first. + #[test] + fn public_ids_are_unique_per_cloud() { + let mut seen = std::collections::HashSet::new(); + for model in super::CATALOG { + assert!( + seen.insert((model.cloud, model.public_id)), + "public id '{}' appears more than once under {:?}", + model.public_id, + model.cloud + ); + } + } + + use super::*; + + #[test] + fn resolve_accepts_anthropic_native_spellings() { + // Claude Code /model forms: dashed minor version, with and without date. + assert_eq!(resolve("claude-haiku-4-5").unwrap().public_id, "claude-haiku-4.5"); + assert_eq!( + resolve("claude-sonnet-4-5-20250929").unwrap().public_id, + "claude-sonnet-4.5" + ); + // Full Bedrock upstream ids, with geo/vendor prefix and version suffix. + assert_eq!( + resolve("us.anthropic.claude-haiku-4-5-20251001-v1:0").unwrap().public_id, + "claude-haiku-4.5" + ); + assert_eq!( + resolve("anthropic.claude-opus-4-6-v1").unwrap().public_id, + "claude-opus-4.6" + ); + // Whole versions are already catalog form. + assert_eq!(resolve("claude-sonnet-5").unwrap().public_id, "claude-sonnet-5"); + // Exact public ids still win untouched. + assert_eq!(resolve("claude-opus-4.8").unwrap().public_id, "claude-opus-4.8"); + assert_eq!(resolve("gpt-oss-20b").unwrap().public_id, "gpt-oss-20b"); + // Unknowns stay unknown — no fuzzy matching. + assert!(resolve("claude-nonexistent-9-9").is_none()); + assert!(resolve("gpt-5").is_none()); + } + + #[test] + fn aws_has_openai_and_anthropic_with_plain_ids() { + let aws = models_for(Platform::Aws); + assert!(!aws.is_empty()); + assert!(aws + .iter() + .any(|m| m.public_id == "gpt-oss-20b" && m.protocol == Protocol::OpenAi)); + assert!( + aws.iter().any(|m| m.protocol == Protocol::Anthropic), + "Claude must be included via the Anthropic protocol" + ); + // The OpenAI endpoint rejects `us.*` cross-region profile ids. + assert!(aws.iter().all(|m| !m.upstream_id.starts_with("us."))); + } + + #[test] + fn resolve_for_scopes_to_cloud() { + // The same public id serves on more than one cloud with different upstream + // ids, so resolution must scope to the binding's cloud. + let aws = resolve_for("claude-opus-4.8", Platform::Aws).expect("aws claude"); + assert_eq!(aws.upstream_id, "anthropic.claude-opus-4-8"); + let gcp = resolve_for("claude-opus-4.8", Platform::Gcp).expect("gcp claude"); + assert_eq!(gcp.upstream_id, "claude-opus-4-8"); + assert_eq!(gcp.protocol, Protocol::Anthropic); + // Canonicalization applies per cloud: Claude Code's dashed release-date + // spelling resolves to the Vertex `@date` id. + let dated = resolve_for("claude-haiku-4-5-20251001", Platform::Gcp).expect("dated id"); + assert_eq!(dated.upstream_id, "claude-haiku-4-5@20251001"); + // A Vertex-native `@date` spelling resolves too — it is the very id the + // GCP catalog stores upstream. + let vertex = resolve_for("claude-sonnet-4-5@20250929", Platform::Gcp).expect("vertex id"); + assert_eq!(vertex.upstream_id, "claude-sonnet-4-5@20250929"); + // A model serving on one cloud does not resolve on another. + assert!(resolve_for("gemini-2.5-pro", Platform::Aws).is_none()); + assert!(resolve_for("gpt-4.1", Platform::Gcp).is_none()); + } + + #[test] + fn lookup_round_trips() { + let m = lookup("gpt-oss-20b").expect("known model"); + assert_eq!(m.cloud, Platform::Aws); + assert_eq!(m.protocol, Protocol::OpenAi); + assert_eq!(m.upstream_id, "openai.gpt-oss-20b-1:0"); + + let c = lookup("claude-opus-4.8").expect("claude known"); + assert_eq!(c.protocol, Protocol::Anthropic); + + assert!(lookup("nonexistent-model").is_none()); + } + + #[test] + fn azure_deployments_map_to_catalog() { + assert!(!azure_deployments().is_empty()); + for (deployment, _, _) in azure_deployments() { + assert!( + models_for(Platform::Azure) + .iter() + .any(|m| m.upstream_id == deployment), + "azure deployment {deployment} must map to a catalog model" + ); + } + } + + #[test] + fn protocol_serializes_lowercase() { + assert_eq!(serde_json::to_string(&Protocol::OpenAi).unwrap(), "\"openai\""); + assert_eq!(serde_json::to_string(&Protocol::Anthropic).unwrap(), "\"anthropic\""); + } +} diff --git a/crates/alien-core/src/bin/schema_exporter.rs b/crates/alien-core/src/bin/schema_exporter.rs index 36e3fe441..d46e8fb76 100644 --- a/crates/alien-core/src/bin/schema_exporter.rs +++ b/crates/alien-core/src/bin/schema_exporter.rs @@ -91,6 +91,10 @@ use utoipa::OpenApi; AwsOpenSearchOutputs, Postgres, PostgresOutputs, + Ai, + AiOutputs, + FinetuneSpec, + FinetuneMethod, Queue, QueueOutputs, Email, diff --git a/crates/alien-core/src/bindings/ai.rs b/crates/alien-core/src/bindings/ai.rs new file mode 100644 index 000000000..48fd56c72 --- /dev/null +++ b/crates/alien-core/src/bindings/ai.rs @@ -0,0 +1,371 @@ +//! AI Gateway binding definitions for managed AI inference across cloud providers. + +use super::BindingValue; +use serde::{Deserialize, Serialize}; + +/// Represents an AI Gateway binding for managed inference across cloud providers. +/// +/// The managed variants (Bedrock/Vertex/Foundry) carry only identifiers and +/// endpoints; authentication uses the workload's ambient cloud identity. The +/// `External` (BYO-key) variant deliberately carries a vault-resolved API key. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(tag = "service", rename_all = "lowercase")] +pub enum AiBinding { + /// AWS Bedrock AI binding + Bedrock(BedrockAiBinding), + /// GCP Vertex AI binding + Vertex(VertexAiBinding), + /// Azure AI Foundry binding + Foundry(FoundryAiBinding), + /// External provider binding (generic endpoint-based) + External(ExternalAiBinding), +} + +/// A tuned model the gateway can route to, produced by a completed +/// fine-tuning job. Maps the public `served_id` an app requests to the +/// provider-native artifact the gateway forwards to (a Bedrock custom-model +/// ARN, a Vertex tuned endpoint id, or a Foundry deployment name). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct TunedModel { + /// The public model id apps send in the `model` field. + pub served_id: String, + /// The provider-native upstream artifact (custom-model ARN / tuned endpoint + /// id / deployment name) the gateway forwards to. + pub upstream_id: String, +} + +/// The fine-tuning capability a managed binding carries when its `Ai` resource +/// declared `.finetune(...)`. This is *not* a completed tuned model — it is the +/// declaration the gateway needs to submit a tuning job at runtime (when the app +/// calls `POST //v1/finetune`) and to rediscover the tuned model by +/// convention once the job completes. Absent for a pure inference gateway. +/// +/// The tuning job runs at runtime, not at deploy time, so this capability travels +/// on the binding while `tuned_model` is populated only after a job succeeds (via +/// rediscovery, so it may be absent even when a capability is present). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct FinetuneCapability { + /// Provider-native base-model id the job tunes. + pub base_model: String, + /// The object-storage bucket (S3 / GCS / Blob) holding the training dataset. + /// Resolved by the controller from the training-data storage dependency. + pub training_bucket: String, + /// Object key of the training file within `training_bucket`. + pub training_key: String, + /// The public model id the tuned model is served under. + pub served_model_id: String, + /// The deterministic provider-side name of the tuned model / job, used both to + /// submit and to rediscover it (e.g. the Bedrock custom-model + job name). + pub job_name: String, + /// IAM role ARN (AWS) the tuning job assumes to read/write S3. Empty on clouds + /// that submit under the ambient identity without a passed role. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub role_arn: String, +} + +/// AWS Bedrock AI binding configuration +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct BedrockAiBinding { + /// The AWS region where Bedrock is accessed + pub region: String, + /// A tuned model served alongside the base catalog, if the resource + /// declared a completed fine-tuning job. Absent for a pure inference gateway. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tuned_model: Option, + /// The fine-tuning capability the gateway uses to submit and rediscover a + /// runtime tuning job. Present when the resource declared `.finetune(...)`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub finetune: Option, +} + +/// GCP Vertex AI binding configuration +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct VertexAiBinding { + /// The GCP project ID + pub project: String, + /// The Vertex AI region (e.g., "us-central1") + pub location: String, + /// A tuned model served alongside the base catalog, if the resource + /// declared a completed fine-tuning job. Absent for a pure inference gateway. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tuned_model: Option, + /// The fine-tuning capability the gateway uses to submit and rediscover a + /// runtime tuning job. Present when the resource declared `.finetune(...)`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub finetune: Option, +} + +/// Azure AI Foundry binding configuration +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct FoundryAiBinding { + /// The Foundry deployment endpoint URL + pub endpoint: String, + /// The Azure account or subscription identifier + pub account: String, + /// A tuned model served alongside the base catalog, if the resource + /// declared a completed fine-tuning job. Absent for a pure inference gateway. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tuned_model: Option, + /// The fine-tuning capability the gateway uses to submit and rediscover a + /// runtime tuning job. Present when the resource declared `.finetune(...)`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub finetune: Option, +} + +/// External AI provider binding configuration (BYO-key). +/// +/// The operator-supplied secret rides inside the binding via +/// `BindingValue`, so it is a literal on cloud platforms and gains +/// Kubernetes SecretRef resolution for free (`extract_binding_secrets` walks +/// the binding JSON for `secretRef`). +// No derived `Debug` — an inline `api_key` would print cleartext; see the redacting impl below. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct ExternalAiBinding { + /// The external AI provider name (e.g., "openai", "anthropic") + pub provider: String, + /// The provider API key. Resolved to plaintext in the worker environment + /// (literal on cloud, Kubernetes Secret on K8s) so the SDK reads it directly. + pub api_key: BindingValue, +} + +// Redacts the inline key and keeps every other field, mirroring the external +// Postgres binding's redacting impl. +impl std::fmt::Debug for ExternalAiBinding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExternalAiBinding") + .field("provider", &self.provider) + .field("api_key", &"") + .finish() + } +} + +impl AiBinding { + /// The env var a developer sets to bring their own provider key on the Local platform. + /// Shared by the Local controller (provision-time check) and the local bindings + /// resolver (runtime-only re-resolution), so the two never drift. + pub const LOCAL_API_KEY_ENV: &'static str = "OPENAI_API_KEY"; + /// The BYO-key provider assumed on the Local platform. + pub const LOCAL_DEFAULT_PROVIDER: &'static str = "openai"; + + pub fn bedrock(region: impl Into) -> Self { + Self::Bedrock(BedrockAiBinding { + region: region.into(), + tuned_model: None, + finetune: None, + }) + } + + pub fn vertex(project: impl Into, location: impl Into) -> Self { + Self::Vertex(VertexAiBinding { + project: project.into(), + location: location.into(), + tuned_model: None, + finetune: None, + }) + } + + pub fn foundry(endpoint: impl Into, account: impl Into) -> Self { + Self::Foundry(FoundryAiBinding { + endpoint: endpoint.into(), + account: account.into(), + tuned_model: None, + finetune: None, + }) + } + + /// Attach a tuned model to a managed binding, so the gateway routes + /// `served_id` to `upstream_id` alongside the base catalog. A no-op on the + /// `External` (BYO-key) variant, which the gateway does not serve. + pub fn with_tuned_model(self, served_id: impl Into, upstream_id: impl Into) -> Self { + let tuned = TunedModel { + served_id: served_id.into(), + upstream_id: upstream_id.into(), + }; + match self { + Self::Bedrock(b) => Self::Bedrock(BedrockAiBinding { + tuned_model: Some(tuned), + ..b + }), + Self::Vertex(b) => Self::Vertex(VertexAiBinding { + tuned_model: Some(tuned), + ..b + }), + Self::Foundry(b) => Self::Foundry(FoundryAiBinding { + tuned_model: Some(tuned), + ..b + }), + Self::External(b) => Self::External(b), + } + } + + /// Attach a fine-tuning capability to a managed binding, so the gateway can + /// submit and rediscover a runtime tuning job. A no-op on `External`. + pub fn with_finetune(self, capability: FinetuneCapability) -> Self { + match self { + Self::Bedrock(b) => Self::Bedrock(BedrockAiBinding { + finetune: Some(capability), + ..b + }), + Self::Vertex(b) => Self::Vertex(VertexAiBinding { + finetune: Some(capability), + ..b + }), + Self::Foundry(b) => Self::Foundry(FoundryAiBinding { + finetune: Some(capability), + ..b + }), + Self::External(b) => Self::External(b), + } + } + + /// The tuned model attached to this binding, if any. + pub fn tuned_model(&self) -> Option<&TunedModel> { + match self { + Self::Bedrock(b) => b.tuned_model.as_ref(), + Self::Vertex(b) => b.tuned_model.as_ref(), + Self::Foundry(b) => b.tuned_model.as_ref(), + Self::External(_) => None, + } + } + + /// The fine-tuning capability attached to this binding, if any. + pub fn finetune(&self) -> Option<&FinetuneCapability> { + match self { + Self::Bedrock(b) => b.finetune.as_ref(), + Self::Vertex(b) => b.finetune.as_ref(), + Self::Foundry(b) => b.finetune.as_ref(), + Self::External(_) => None, + } + } + + pub fn external( + provider: impl Into, + api_key: impl Into>, + ) -> Self { + Self::External(ExternalAiBinding { + provider: provider.into(), + api_key: api_key.into(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bedrock_binding_roundtrip() { + let binding = AiBinding::bedrock("us-east-1"); + + let json = serde_json::to_string(&binding).unwrap(); + assert!(json.contains(r#""service":"bedrock""#)); + + let deserialized: AiBinding = serde_json::from_str(&json).unwrap(); + assert_eq!(binding, deserialized); + } + + #[test] + fn test_bedrock_binding_without_tuned_model_omits_field() { + let binding = AiBinding::bedrock("us-east-1"); + let json = serde_json::to_value(&binding).unwrap(); + assert!( + json.get("tunedModel").is_none(), + "an untuned binding must omit tunedModel so the inference-only wire shape is unchanged" + ); + } + + #[test] + fn test_bedrock_binding_with_tuned_model_roundtrip() { + let binding = AiBinding::bedrock("us-east-1") + .with_tuned_model("finance-model", "arn:aws:bedrock:us-east-1:123:custom-model/abc"); + + let json = serde_json::to_value(&binding).unwrap(); + assert_eq!(json["service"], "bedrock"); + assert_eq!(json["tunedModel"]["servedId"], "finance-model"); + assert_eq!( + json["tunedModel"]["upstreamId"], + "arn:aws:bedrock:us-east-1:123:custom-model/abc" + ); + + let back: AiBinding = serde_json::from_value(json).unwrap(); + assert_eq!(binding, back); + assert_eq!(back.tuned_model().unwrap().served_id, "finance-model"); + } + + #[test] + fn test_external_binding_ignores_tuned_model() { + // The gateway does not serve BYO-key providers, so a tuned model is a no-op there. + let binding = AiBinding::external("openai", "sk-x").with_tuned_model("x", "y"); + assert!(binding.tuned_model().is_none()); + } + + #[test] + fn test_vertex_binding_roundtrip() { + let binding = AiBinding::vertex("my-project", "us-central1"); + + let json = serde_json::to_string(&binding).unwrap(); + assert!(json.contains(r#""service":"vertex""#)); + + let deserialized: AiBinding = serde_json::from_str(&json).unwrap(); + assert_eq!(binding, deserialized); + } + + #[test] + fn test_foundry_binding_roundtrip() { + let binding = AiBinding::foundry("https://my-foundry.openai.azure.com", "my-subscription"); + + let json = serde_json::to_value(&binding).unwrap(); + let json_str = json.to_string(); + assert!(json_str.contains(r#""service":"foundry""#)); + assert!( + !json_str.contains("instantAccess"), + "foundry binding must not serialize instant_access" + ); + + let deserialized: AiBinding = serde_json::from_value(json).unwrap(); + assert_eq!(binding, deserialized); + } + + #[test] + fn test_external_binding_roundtrip() { + let binding = AiBinding::external("openai", "sk-test-key"); + + // The injected env-var JSON must match exactly what the SDK's + // `ai(name)` parser expects: service-tagged, camelCase, key inline. + let json = serde_json::to_value(&binding).unwrap(); + let json_str = json.to_string(); + assert!(json_str.contains(r#""apiKey""#), "external binding must serialize apiKey in camelCase"); + assert_eq!( + json, + serde_json::json!({ + "service": "external", + "provider": "openai", + "apiKey": "sk-test-key", + }) + ); + + let deserialized: AiBinding = + serde_json::from_value(json).expect("external binding should round-trip"); + assert_eq!(binding, deserialized); + } +} diff --git a/crates/alien-core/src/bindings/mod.rs b/crates/alien-core/src/bindings/mod.rs index 793102de1..eca2b1d07 100644 --- a/crates/alien-core/src/bindings/mod.rs +++ b/crates/alien-core/src/bindings/mod.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use std::collections::HashMap; +mod ai; mod artifact_registry; mod build; mod container; @@ -26,6 +27,10 @@ mod storage; mod vault; mod worker; +pub use ai::{ + AiBinding, BedrockAiBinding, ExternalAiBinding, FinetuneCapability, FoundryAiBinding, + TunedModel, VertexAiBinding, +}; pub use artifact_registry::{ AcrArtifactRegistryBinding, ArtifactRegistryBinding, EcrArtifactRegistryBinding, GarArtifactRegistryBinding, LocalArtifactRegistryBinding, diff --git a/crates/alien-core/src/external_bindings.rs b/crates/alien-core/src/external_bindings.rs index a01f20868..432be3677 100644 --- a/crates/alien-core/src/external_bindings.rs +++ b/crates/alien-core/src/external_bindings.rs @@ -10,8 +10,8 @@ use alien_error::AlienError; use serde::{Deserialize, Serialize}; use crate::bindings::{ - ArtifactRegistryBinding, BindingValue, ContainerAppsEnvironmentBinding, KvBinding, - PostgresBinding, QueueBinding, StorageBinding, VaultBinding, + ArtifactRegistryBinding, BindingValue, ContainerAppsEnvironmentBinding, ExternalAiBinding, + KvBinding, PostgresBinding, QueueBinding, StorageBinding, VaultBinding, }; use crate::error::ErrorData; use crate::resource::ResourceOutputs; @@ -40,6 +40,8 @@ pub enum ExternalBinding { ContainerAppsEnvironment(ContainerAppsEnvironmentBinding), /// External Postgres binding (operator-provided / BYO database) Postgres(PostgresBinding), + /// External AI provider binding (BYO-key OpenAI/Anthropic, etc.) + Ai(ExternalAiBinding), } /// Map from resource ID to external binding. @@ -144,6 +146,20 @@ impl ExternalBindings { } } + /// Gets an AI binding for the given resource ID. + /// Returns an error if the binding exists but is not an Ai type. + pub fn get_ai(&self, id: &str) -> crate::error::Result> { + match self.0.get(id) { + Some(ExternalBinding::Ai(b)) => Ok(Some(b)), + Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch { + resource_id: id.to_string(), + expected: "ai".to_string(), + actual: other.binding_type().to_string(), + })), + None => Ok(None), + } + } + /// Gets a container apps environment binding for the given resource ID. /// Returns an error if the binding exists but is not a ContainerAppsEnvironment type. pub fn get_container_apps_environment( @@ -191,6 +207,7 @@ impl ExternalBinding { ExternalBinding::Vault(_) => "vault", ExternalBinding::ContainerAppsEnvironment(_) => "azure_container_apps_environment", ExternalBinding::Postgres(_) => "postgres", + ExternalBinding::Ai(_) => "ai", } } @@ -241,6 +258,28 @@ impl ExternalBinding { _ => None, } } + + /// Serializes this external binding to the JSON value injected into the worker + /// environment as `ALIEN__BINDING`, matching the shape the SDK parses. + /// + /// The AI arm wraps the carried `ExternalAiBinding` in `AiBinding::External` so + /// the value keeps the `service` tag the SDK's `ai(name)` parser discriminates + /// on; a bare `ExternalAiBinding` would lack it. Every other arm serializes the + /// carried binding directly, like the runtime controllers' `get_binding_params`. + pub fn to_env_binding_value(&self) -> serde_json::Result { + match self { + ExternalBinding::Storage(b) => serde_json::to_value(b), + ExternalBinding::Queue(b) => serde_json::to_value(b), + ExternalBinding::Kv(b) => serde_json::to_value(b), + ExternalBinding::ArtifactRegistry(b) => serde_json::to_value(b), + ExternalBinding::Vault(b) => serde_json::to_value(b), + ExternalBinding::ContainerAppsEnvironment(b) => serde_json::to_value(b), + ExternalBinding::Postgres(b) => serde_json::to_value(b), + ExternalBinding::Ai(b) => { + serde_json::to_value(crate::bindings::AiBinding::External(b.clone())) + } + } + } } /// Validates that an external binding type matches the resource type. @@ -259,6 +298,7 @@ pub fn validate_binding_type( ("vault", ExternalBinding::Vault(_)) => true, ("azure_container_apps_environment", ExternalBinding::ContainerAppsEnvironment(_)) => true, ("postgres", ExternalBinding::Postgres(_)) => true, + ("ai", ExternalBinding::Ai(_)) => true, _ => false, }; @@ -303,6 +343,25 @@ mod tests { assert!(bindings.get_storage("cache").is_err()); // Wrong type } + #[test] + fn test_external_bindings_ai() { + use crate::bindings::ExternalAiBinding; + + let mut bindings = ExternalBindings::new(); + bindings.insert( + "llm", + ExternalBinding::Ai(ExternalAiBinding { + provider: "openai".to_string(), + api_key: "sk-test".into(), + }), + ); + + assert!(bindings.has("llm")); + assert_eq!(bindings.get("llm").unwrap().binding_type(), "ai"); + assert_eq!(bindings.get_ai("llm").unwrap().unwrap().provider, "openai"); + assert!(bindings.get_kv("llm").is_err()); // Wrong type + } + #[test] fn test_external_bindings_serialization() { let mut bindings = ExternalBindings::new(); diff --git a/crates/alien-core/src/heartbeat.rs b/crates/alien-core/src/heartbeat.rs index 7f35c24c8..5aad9249c 100644 --- a/crates/alien-core/src/heartbeat.rs +++ b/crates/alien-core/src/heartbeat.rs @@ -156,6 +156,8 @@ pub enum ResourceHeartbeatData { AzureContainerAppsEnvironment(AzureContainerAppsEnvironmentHeartbeatData), #[serde(rename = "azure_service_bus_namespace")] AzureServiceBusNamespace(AzureServiceBusNamespaceHeartbeatData), + #[serde(rename = "ai")] + Ai(AiHeartbeatData), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -1527,6 +1529,43 @@ impl Default for PostgresHeartbeatStatus { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(tag = "backend", rename_all = "camelCase")] +pub enum AiHeartbeatData { + AwsBedrock(AwsBedrockAiHeartbeatData), + GcpVertex(GcpVertexAiHeartbeatData), + AzureFoundry(AzureFoundryAiHeartbeatData), + External(ExternalAiHeartbeatData), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AiHeartbeatStatus { + pub health: ObservedHealth, + pub lifecycle: ProviderLifecycleState, + pub message: Option, + pub stale: bool, + pub partial: bool, + pub collection_issues: Vec, +} + +impl Default for AiHeartbeatStatus { + fn default() -> Self { + // No per-stack resource to poll, so unobserved = healthy/running; + // errors surface at inference time via the SDK. + Self { + health: ObservedHealth::Healthy, + lifecycle: ProviderLifecycleState::Running, + message: None, + stale: false, + partial: false, + collection_issues: vec![], + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] @@ -1580,6 +1619,44 @@ pub struct AzureFlexibleServerPostgresHeartbeatData { pub version: Option, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AwsBedrockAiHeartbeatData { + pub status: AiHeartbeatStatus, + pub region: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct GcpVertexAiHeartbeatData { + pub status: AiHeartbeatStatus, + pub project: String, + pub location: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AzureFoundryAiHeartbeatData { + pub status: AiHeartbeatStatus, + pub account_name: String, + pub endpoint: Option, + pub resource_group: Option, + pub location: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct ExternalAiHeartbeatData { + pub status: AiHeartbeatStatus, + /// The BYO-key provider serving this binding (e.g. "openai"). Used on the Local + /// platform, where the app brings its own provider key instead of an ambient cloud. + pub provider: String, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(tag = "backend", rename_all = "camelCase")] @@ -2726,4 +2803,53 @@ mod tests { assert!(kv["data"].get("summary").is_none()); assert_eq!(kv["data"]["itemCount"], json!(null)); } + + #[test] + fn ai_heartbeat_serializes_stable_tags() { + let aws = serde_json::to_value(ResourceHeartbeatData::Ai(AiHeartbeatData::AwsBedrock( + AwsBedrockAiHeartbeatData { + status: AiHeartbeatStatus::default(), + region: "us-east-1".to_string(), + }, + ))) + .unwrap(); + let gcp = + serde_json::to_value(ResourceHeartbeatData::Ai(AiHeartbeatData::GcpVertex( + GcpVertexAiHeartbeatData { + status: AiHeartbeatStatus::default(), + project: "my-project".to_string(), + location: "us-central1".to_string(), + }, + ))) + .unwrap(); + let azure = + serde_json::to_value(ResourceHeartbeatData::Ai(AiHeartbeatData::AzureFoundry( + AzureFoundryAiHeartbeatData { + status: AiHeartbeatStatus::default(), + account_name: "my-ai-account".to_string(), + endpoint: Some("https://my-ai-account.openai.azure.com/".to_string()), + resource_group: Some("my-rg".to_string()), + location: Some("eastus".to_string()), + }, + ))) + .unwrap(); + + assert_eq!(aws["resourceType"], "ai"); + assert_eq!(aws["data"]["backend"], "awsBedrock"); + assert_eq!(aws["data"]["region"], "us-east-1"); + assert!(aws["data"].get("summary").is_none()); + + assert_eq!(gcp["resourceType"], "ai"); + assert_eq!(gcp["data"]["backend"], "gcpVertex"); + assert_eq!(gcp["data"]["project"], "my-project"); + assert_eq!(gcp["data"]["location"], "us-central1"); + + assert_eq!(azure["resourceType"], "ai"); + assert_eq!(azure["data"]["backend"], "azureFoundry"); + assert_eq!(azure["data"]["accountName"], "my-ai-account"); + assert_eq!( + azure["data"]["endpoint"], + "https://my-ai-account.openai.azure.com/" + ); + } } diff --git a/crates/alien-core/src/import/data/aws/ai.rs b/crates/alien-core/src/import/data/aws/ai.rs new file mode 100644 index 000000000..e00354ef5 --- /dev/null +++ b/crates/alien-core/src/import/data/aws/ai.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +/// AWS AI ImportData. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct AwsAiImportData { + /// AWS region where Bedrock is accessed. + pub region: String, +} diff --git a/crates/alien-core/src/import/data/aws/mod.rs b/crates/alien-core/src/import/data/aws/mod.rs index 020715aa3..b6cff9817 100644 --- a/crates/alien-core/src/import/data/aws/mod.rs +++ b/crates/alien-core/src/import/data/aws/mod.rs @@ -1,3 +1,4 @@ +pub mod ai; pub mod artifact_registry; pub mod build; pub mod compute_cluster; @@ -13,6 +14,7 @@ pub mod storage; pub mod vault; pub mod worker; +pub use ai::*; pub use artifact_registry::*; pub use build::*; pub use compute_cluster::*; diff --git a/crates/alien-core/src/import/data/azure/ai.rs b/crates/alien-core/src/import/data/azure/ai.rs new file mode 100644 index 000000000..eefa5286d --- /dev/null +++ b/crates/alien-core/src/import/data/azure/ai.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +/// Azure AI (AIServices) ImportData. +/// +/// Carries the account name, endpoint, resource group, and location from an +/// externally-created AIServices account directly into the controller's ready +/// state so that heartbeat ticks and binding-param serialization work without +/// a cloud round-trip. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct AzureAiImportData { + /// Name of the Azure CognitiveServices / AIServices account. + pub account_name: String, + /// The endpoint URL of the AIServices account. + pub endpoint: String, + /// Azure resource group containing the account. + pub resource_group: String, + /// Azure region where the account lives (e.g. "eastus"). + pub location: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The Azure AI Terraform emitter writes the import ref with camelCase keys + /// (`crates/alien-terraform/src/emitters/azure/ai.rs`); it also emits an extra + /// `subscriptionId` this struct does not read. This pins the four consumed keys so a + /// future rename can't silently break the frozen/Terraform import round-trip. + #[test] + fn deserializes_the_emitter_import_ref_keys() { + let json = serde_json::json!({ + "subscriptionId": "sub-ignored", + "resourceGroup": "my-rg", + "accountName": "my-account", + "endpoint": "https://my-account.openai.azure.com/", + "location": "eastus", + }); + let data: AzureAiImportData = + serde_json::from_value(json).expect("the emitter's import-ref keys must deserialize"); + assert_eq!(data.account_name, "my-account"); + assert_eq!(data.endpoint, "https://my-account.openai.azure.com/"); + assert_eq!(data.resource_group, "my-rg"); + assert_eq!(data.location, "eastus"); + } +} diff --git a/crates/alien-core/src/import/data/azure/mod.rs b/crates/alien-core/src/import/data/azure/mod.rs index 588c78c9a..d21075016 100644 --- a/crates/alien-core/src/import/data/azure/mod.rs +++ b/crates/alien-core/src/import/data/azure/mod.rs @@ -1,3 +1,4 @@ +pub mod ai; pub mod artifact_registry; pub mod build; pub mod compute_cluster; @@ -16,6 +17,7 @@ pub mod storage_account; pub mod vault; pub mod worker; +pub use ai::*; pub use artifact_registry::*; pub use build::*; pub use compute_cluster::*; diff --git a/crates/alien-core/src/import/data/gcp/ai.rs b/crates/alien-core/src/import/data/gcp/ai.rs new file mode 100644 index 000000000..58a557c68 --- /dev/null +++ b/crates/alien-core/src/import/data/gcp/ai.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +/// GCP AI (Vertex AI) ImportData. +/// +/// The `project_id` and `location` fields identify the Vertex AI endpoint and +/// are carried directly into the controller's ready state so that heartbeat +/// ticks and binding-param serialization work without a cloud round-trip. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct GcpAiImportData { + /// GCP project ID that owns the Vertex AI endpoint. + pub project_id: String, + /// GCP region (location) of the Vertex AI endpoint. + pub location: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The GCP AI Terraform emitter writes the import ref with key `projectId` + /// (`crates/alien-terraform/src/emitters/gcp/ai.rs`), matching every other GCP + /// resource. This pins the serde key so a frozen/Terraform import round-trip does not + /// fail with `missing field project`. + #[test] + fn deserializes_the_emitter_import_ref_key() { + let json = serde_json::json!({ "projectId": "my-project", "location": "us-central1" }); + let data: GcpAiImportData = + serde_json::from_value(json).expect("emitter's projectId key must deserialize"); + assert_eq!(data.project_id, "my-project"); + assert_eq!(data.location, "us-central1"); + } +} diff --git a/crates/alien-core/src/import/data/gcp/mod.rs b/crates/alien-core/src/import/data/gcp/mod.rs index cf77c95b8..663382061 100644 --- a/crates/alien-core/src/import/data/gcp/mod.rs +++ b/crates/alien-core/src/import/data/gcp/mod.rs @@ -1,3 +1,4 @@ +pub mod ai; pub mod artifact_registry; pub mod build; pub mod compute_cluster; @@ -12,6 +13,7 @@ pub mod storage; pub mod vault; pub mod worker; +pub use ai::*; pub use artifact_registry::*; pub use build::*; pub use compute_cluster::*; diff --git a/crates/alien-core/src/import/data/mod.rs b/crates/alien-core/src/import/data/mod.rs index 4dabb9a9a..2885f7ef2 100644 --- a/crates/alien-core/src/import/data/mod.rs +++ b/crates/alien-core/src/import/data/mod.rs @@ -8,23 +8,23 @@ pub mod gcp; pub mod kubernetes_cluster; pub use aws::{ - AwsArtifactRegistryImportData, AwsBuildImportData, AwsComputeClusterImportData, + AwsAiImportData, AwsArtifactRegistryImportData, AwsBuildImportData, AwsComputeClusterImportData, AwsEmailDkimTokenImportData, AwsEmailDomainImportData, AwsEmailImportData, AwsKvImportData, AwsNetworkImportData, AwsOpenSearchImportData, AwsPostgresImportData, AwsQueueImportData, AwsRemoteStackManagementImportData, AwsServiceAccountImportData, AwsStorageImportData, AwsVaultImportData, AwsWorkerImportData, }; pub use azure::{ - AzureArtifactRegistryImportData, AzureBuildImportData, AzureComputeClusterImportData, - AzureContainerAppsEnvironmentImportData, AzureFlexibleServerPostgresImportData, - AzureKvImportData, AzureNetworkImportData, AzureQueueImportData, - AzureRemoteStackManagementImportData, AzureResourceGroupImportData, + AzureAiImportData, AzureArtifactRegistryImportData, AzureBuildImportData, + AzureComputeClusterImportData, AzureContainerAppsEnvironmentImportData, + AzureFlexibleServerPostgresImportData, AzureKvImportData, AzureNetworkImportData, + AzureQueueImportData, AzureRemoteStackManagementImportData, AzureResourceGroupImportData, AzureServiceAccountImportData, AzureServiceActivationImportData, AzureServiceBusNamespaceImportData, AzureStorageAccountImportData, AzureStorageImportData, AzureVaultImportData, AzureWorkerImportData, }; pub use gcp::{ - GcpArtifactRegistryImportData, GcpBuildImportData, GcpComputeClusterImportData, + GcpAiImportData, GcpArtifactRegistryImportData, GcpBuildImportData, GcpComputeClusterImportData, GcpKvImportData, GcpNetworkImportData, GcpPostgresImportData, GcpQueueImportData, GcpRemoteStackManagementImportData, GcpServiceAccountImportData, GcpServiceActivationImportData, GcpStorageImportData, GcpVaultImportData, GcpWorkerImportData, diff --git a/crates/alien-core/src/lib.rs b/crates/alien-core/src/lib.rs index 871179a3d..fc2b29762 100644 --- a/crates/alien-core/src/lib.rs +++ b/crates/alien-core/src/lib.rs @@ -60,6 +60,7 @@ pub use stack_settings::*; mod public_urls; pub use public_urls::*; +pub mod ai_catalog; pub mod bindings; pub use bindings::*; diff --git a/crates/alien-core/src/ownership.rs b/crates/alien-core/src/ownership.rs index c894d5d08..557e4641e 100644 --- a/crates/alien-core/src/ownership.rs +++ b/crates/alien-core/src/ownership.rs @@ -96,7 +96,7 @@ pub fn ownership_policy_for_resource_type(resource_type: &str) -> ResourceOwners // Durable search state, setup-owned only: there is no runtime // controller that could provision or replace the collection. "experimental/aws-opensearch" => frozen_only(), - "storage" | "queue" | "kv" | "vault" | "postgres" => user_choice(), + "storage" | "queue" | "kv" | "vault" | "postgres" | "ai" => user_choice(), _ => user_choice(), } } @@ -175,7 +175,7 @@ mod tests { #[test] fn data_resources_can_be_frozen_or_live() { - for resource_type in ["storage", "queue", "kv", "vault", "postgres"] { + for resource_type in ["storage", "queue", "kv", "vault", "postgres", "ai"] { let policy = ownership_policy_for_resource_type(resource_type); assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen); assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen)); diff --git a/crates/alien-core/src/resource.rs b/crates/alien-core/src/resource.rs index b171ce35e..4f2f309ab 100644 --- a/crates/alien-core/src/resource.rs +++ b/crates/alien-core/src/resource.rs @@ -214,6 +214,10 @@ impl<'de> Deserialize<'de> for Resource { serde_json::from_value::(value) .map_err(serde::de::Error::custom)?, ), + "ai" => Box::new( + serde_json::from_value::(value) + .map_err(serde::de::Error::custom)?, + ), "network" => Box::new( serde_json::from_value::(value) .map_err(serde::de::Error::custom)?, @@ -273,6 +277,7 @@ impl<'de> Deserialize<'de> for Resource { "email", "kv", "postgres", + "ai", "network", "build", "service-account", @@ -560,6 +565,10 @@ impl<'de> Deserialize<'de> for ResourceOutputs { serde_json::from_value::(value) .map_err(serde::de::Error::custom)?, ), + "ai" => Box::new( + serde_json::from_value::(value) + .map_err(serde::de::Error::custom)?, + ), "network" => Box::new( serde_json::from_value::(value) .map_err(serde::de::Error::custom)?, @@ -624,6 +633,7 @@ impl<'de> Deserialize<'de> for ResourceOutputs { "email", "kv", "postgres", + "ai", "network", "build", "service-account", diff --git a/crates/alien-core/src/resources/ai.rs b/crates/alien-core/src/resources/ai.rs new file mode 100644 index 000000000..a1474e055 --- /dev/null +++ b/crates/alien-core/src/resources/ai.rs @@ -0,0 +1,414 @@ +use crate::error::{ErrorData, Result}; +use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef}; +use crate::{ResourceType, Storage}; +use alien_error::AlienError; +use bon::Builder; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::fmt::Debug; + +/// The fine-tuning method applied to the base model. +/// +/// The gateway-side controllers map each variant onto the provider's native +/// technique: `Sft` is supervised fine-tuning (all three clouds), `Dpo` is +/// direct preference optimization (Bedrock/Foundry), and `Lora` requests a +/// parameter-efficient adapter where the provider exposes it. Providers that +/// only implement a subset reject unsupported methods at job-submit time. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "lowercase")] +pub enum FinetuneMethod { + /// Supervised fine-tuning on labelled prompt/response pairs (default). + Sft, + /// Direct preference optimization on chosen/rejected pairs. + Dpo, + /// Low-rank adaptation (parameter-efficient) where the provider exposes it. + Lora, +} + +impl Default for FinetuneMethod { + fn default() -> Self { + Self::Sft + } +} + +/// Declares that an [`Ai`] resource should fine-tune a base model in the +/// customer's cloud before serving it. +/// +/// This is a *capability declaration*, not a deploy-time trigger: a resource +/// with a `finetune` spec provisions and is Ready immediately (no job runs at +/// deploy). The declaration flows to the gateway as a fine-tuning capability; +/// the app then starts a job at runtime by calling `ai("").finetune(...)`, +/// which the gateway submits to the provider (Bedrock +/// `CreateModelCustomizationJob`, Vertex tuning job, or Foundry +/// `fine_tuning.jobs`) under the workload's ambient identity, reading the +/// training data from the customer-owned [`Storage`](crate::Storage) bucket +/// (S3 / GCS / Blob) referenced by `training_data`. Once a job completes, the +/// gateway serves the tuned model under `served_model_id`, rediscovering it by +/// convention. Base-model inference is unaffected — an `Ai` without a +/// `finetune` spec behaves exactly as before. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct FinetuneSpec { + /// Provider-native base-model identifier to tune (e.g. an Amazon Nova model + /// id on Bedrock, a Gemini model on Vertex, or a gpt-4o family model on + /// Foundry). Validated against the target cloud when a runtime job is submitted. + pub base_model: String, + + /// The storage resource holding the JSONL training dataset. The gateway + /// reads it from the customer bucket the storage resolves to; the data + /// never leaves the customer's cloud. + pub training_data: String, + + /// Object key of the training file within `training_data`. + /// Defaults to `training.jsonl`. + #[serde(default = "default_training_key", skip_serializing_if = "is_default_training_key")] + pub training_key: String, + + /// The public model id apps use to invoke the tuned model through the + /// gateway (the `model` field in an OpenAI-compatible request). Defaults to + /// `-tuned`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub served_model_id: Option, + + /// The fine-tuning method. Defaults to supervised fine-tuning. + #[serde(default, skip_serializing_if = "is_default_method")] + pub method: FinetuneMethod, +} + +fn default_training_key() -> String { + "training.jsonl".to_string() +} + +fn is_default_training_key(key: &str) -> bool { + key == "training.jsonl" +} + +fn is_default_method(method: &FinetuneMethod) -> bool { + *method == FinetuneMethod::default() +} + +impl FinetuneSpec { + /// The public model id the gateway serves the tuned model under, falling + /// back to `-tuned` when the spec doesn't set one explicitly. + pub fn served_model_id_or_default(&self, ai_id: &str) -> String { + self.served_model_id + .clone() + .unwrap_or_else(|| format!("{ai_id}-tuned")) + } +} + +/// Represents an AI Gateway resource that provides a unified interface to +/// managed AI inference services across cloud providers. +/// +/// BYO-key external providers (OpenAI/Anthropic) are NOT declared here. Like any +/// other BYO infrastructure (e.g. external Redis for `kv`), an external AI +/// provider is supplied at deploy time as an `ExternalBinding::Ai` in the +/// stack's external-bindings map; the executor then skips the cloud controller. +/// +/// When `finetune` is set, the resource also tunes a base model in the +/// customer's cloud and serves the result through the same gateway (see +/// [`FinetuneSpec`]). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[builder(start_fn = new)] +pub struct Ai { + /// Identifier for the AI resource. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]). + /// Maximum 64 characters. + #[builder(start_fn)] + pub id: String, + + /// Optional fine-tuning declaration. When present, the resource tunes + /// `finetune.base_model` on the customer's cloud and serves the result + /// alongside the base models. Absent for a pure inference gateway. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub finetune: Option, +} + +impl Ai { + /// The resource type identifier for AI Gateway + pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("ai"); + + /// Returns the AI resource's unique identifier. + pub fn id(&self) -> &str { + &self.id + } +} + +/// Outputs generated by a successfully provisioned AI Gateway resource. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AiOutputs { + /// The AI provider name (e.g., "bedrock", "vertex", "foundry", "external"). + pub provider: String, + /// The provider endpoint URL, if applicable. + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint: Option, + /// The provider account or project identifier, if applicable. + #[serde(skip_serializing_if = "Option::is_none")] + pub account: Option, +} + +impl ResourceOutputsDefinition for AiOutputs { + fn get_resource_type(&self) -> ResourceType { + Ai::RESOURCE_TYPE.clone() + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool { + other.as_any().downcast_ref::() == Some(self) + } + + fn to_json_value(&self) -> serde_json::Result { + serde_json::to_value(self) + } +} + +impl ResourceDefinition for Ai { + fn get_resource_type(&self) -> ResourceType { + Self::RESOURCE_TYPE + } + + fn id(&self) -> &str { + &self.id + } + + fn get_dependencies(&self) -> Vec { + // The training dataset lives in a customer Storage bucket that must be + // provisioned before the tuning job can read it, so a finetune spec adds + // that storage as a dependency. A pure inference gateway has none. + match &self.finetune { + Some(spec) => vec![ResourceRef::new( + Storage::RESOURCE_TYPE, + spec.training_data.clone(), + )], + None => Vec::new(), + } + } + + fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> { + let new_ai = new_config.as_any().downcast_ref::().ok_or_else(|| { + AlienError::new(ErrorData::UnexpectedResourceType { + resource_id: self.id.clone(), + expected: Self::RESOURCE_TYPE, + actual: new_config.get_resource_type(), + }) + })?; + + if self.id != new_ai.id { + return Err(AlienError::new(ErrorData::InvalidResourceUpdate { + resource_id: self.id.clone(), + reason: "the 'id' field is immutable".to_string(), + })); + } + + // The tuned artifact is derived from the training data + base model, so + // repointing them would silently serve a different model under the same + // served id. Require a new resource id (hence a fresh tuning job) instead. + if let (Some(old), Some(new)) = (&self.finetune, &new_ai.finetune) { + if old.base_model != new.base_model || old.training_data != new.training_data { + return Err(AlienError::new(ErrorData::InvalidResourceUpdate { + resource_id: self.id.clone(), + reason: "finetune 'baseModel' and 'trainingData' are immutable; \ + create a new AI resource to retrain" + .to_string(), + })); + } + } + Ok(()) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool { + other.as_any().downcast_ref::() == Some(self) + } + + fn to_json_value(&self) -> serde_json::Result { + serde_json::to_value(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ai_builder() { + let ai = Ai::new("llm".to_string()).build(); + assert_eq!(ai.id, "llm"); + } + + #[test] + fn test_ai_resource_type() { + assert_eq!(Ai::RESOURCE_TYPE.as_ref(), "ai"); + } + + #[test] + fn test_ai_resource_definition() { + let ai = Ai::new("test-ai".to_string()).build(); + assert_eq!(ai.get_resource_type(), Ai::RESOURCE_TYPE); + assert_eq!(ResourceDefinition::id(&ai), "test-ai"); + assert!(ai.get_dependencies().is_empty()); + } + + #[test] + fn test_ai_validate_update() { + let original = Ai::new("test-ai".to_string()).build(); + let valid_update = Ai::new("test-ai".to_string()).build(); + let invalid_update = Ai::new("different-ai".to_string()).build(); + + assert!(original.validate_update(&valid_update).is_ok()); + assert!(original.validate_update(&invalid_update).is_err()); + } + + #[test] + fn test_ai_finetune_dependency() { + let base = Ai::new("llm".to_string()).build(); + assert!( + base.get_dependencies().is_empty(), + "a pure inference gateway has no dependencies" + ); + + let tuned = Ai::new("llm".to_string()) + .finetune(FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: "training-set".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }) + .build(); + assert_eq!( + tuned.get_dependencies(), + vec![ResourceRef::new(Storage::RESOURCE_TYPE, "training-set")], + "a finetune spec depends on its training-data storage" + ); + } + + #[test] + fn test_ai_served_model_id_default() { + let spec = FinetuneSpec { + base_model: "b".to_string(), + training_data: "d".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }; + assert_eq!(spec.served_model_id_or_default("llm"), "llm-tuned"); + + let explicit = FinetuneSpec { + served_model_id: Some("finance-model".to_string()), + ..spec + }; + assert_eq!(explicit.served_model_id_or_default("llm"), "finance-model"); + } + + #[test] + fn test_ai_finetune_immutable_fields_rejected() { + let original = Ai::new("llm".to_string()) + .finetune(FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: "set-a".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }) + .build(); + + // Same base + data, changed method: allowed. + let ok = Ai::new("llm".to_string()) + .finetune(FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: "set-a".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Dpo, + }) + .build(); + assert!(original.validate_update(&ok).is_ok()); + + // Changed training data: rejected. + let repoint = Ai::new("llm".to_string()) + .finetune(FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: "set-b".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }) + .build(); + let err = original + .validate_update(&repoint) + .expect_err("repointing training data must be rejected"); + assert_eq!(err.code, "INVALID_RESOURCE_UPDATE"); + } + + #[test] + fn test_ai_finetune_roundtrip_camel_case() { + let ai = Ai::new("llm".to_string()) + .finetune(FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: "training-set".to_string(), + training_key: "data.jsonl".to_string(), + served_model_id: Some("finance-model".to_string()), + method: FinetuneMethod::Lora, + }) + .build(); + + let json = serde_json::to_value(&ai).unwrap(); + assert_eq!(json["finetune"]["baseModel"], "amazon.nova-lite-v1:0"); + assert_eq!(json["finetune"]["trainingData"], "training-set"); + assert_eq!(json["finetune"]["trainingKey"], "data.jsonl"); + assert_eq!(json["finetune"]["servedModelId"], "finance-model"); + assert_eq!(json["finetune"]["method"], "lora"); + + let back: Ai = serde_json::from_value(json).unwrap(); + assert_eq!(ai, back); + } + + #[test] + fn test_ai_without_finetune_omits_field() { + let ai = Ai::new("llm".to_string()).build(); + let json = serde_json::to_value(&ai).unwrap(); + assert!( + json.get("finetune").is_none(), + "finetune must be omitted when unset so the inference-only shape is unchanged" + ); + } + + #[test] + fn test_ai_outputs_serialization() { + let outputs = AiOutputs { + provider: "bedrock".to_string(), + endpoint: Some("https://bedrock-runtime.us-east-1.amazonaws.com".to_string()), + account: None, + }; + + let json = serde_json::to_string(&outputs).unwrap(); + let deserialized: AiOutputs = serde_json::from_str(&json).unwrap(); + assert_eq!(outputs, deserialized); + } +} diff --git a/crates/alien-core/src/resources/mod.rs b/crates/alien-core/src/resources/mod.rs index 11faf021c..cf70516d6 100644 --- a/crates/alien-core/src/resources/mod.rs +++ b/crates/alien-core/src/resources/mod.rs @@ -60,6 +60,9 @@ pub use vault::*; mod kv; pub use kv::*; +mod ai; +pub use ai::*; + mod network; pub use network::*; diff --git a/crates/alien-gateway/Cargo.toml b/crates/alien-gateway/Cargo.toml new file mode 100644 index 000000000..f9c539d0f --- /dev/null +++ b/crates/alien-gateway/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "alien-gateway" +version.workspace = true +edition.workspace = true +authors.workspace = true +description.workspace = true +license-file.workspace = true + +[lib] +crate-type = ["rlib"] + +[[bin]] +name = "alien-ai-gateway" +path = "src/bin/alien-ai-gateway.rs" + +[dependencies] +alien-core = { workspace = true } +alien-bindings = { workspace = true, default-features = false, features = ["aws", "gcp", "azure"] } +alien-error = { workspace = true, features = ["axum"] } +tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros"] } +axum = { workspace = true, features = ["http1", "tokio", "json"] } +reqwest = { workspace = true, features = ["json", "stream", "blocking", "rustls-tls-webpki-roots"] } +futures = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +async-trait = { workspace = true } +urlencoding = { workspace = true } +base64 = { workspace = true } +tracing = { workspace = true } +aws-config = { workspace = true } +aws-sigv4 = { workspace = true } +aws-credential-types = { workspace = true } +aws-smithy-eventstream = { workspace = true } +aws-smithy-types = { workspace = true } +http = { workspace = true } + +[dev-dependencies] +httpmock = { workspace = true } +temp-env = { workspace = true } diff --git a/crates/alien-gateway/src/bin/alien-ai-gateway.rs b/crates/alien-gateway/src/bin/alien-ai-gateway.rs new file mode 100644 index 000000000..b8116c871 --- /dev/null +++ b/crates/alien-gateway/src/bin/alien-ai-gateway.rs @@ -0,0 +1,98 @@ +//! Container AI-gateway launcher. A thin bootstrap that hosts the embedded gateway +//! for a workload whose image is not built through alien-runtime (BYO Docker +//! containers). It never supervises the app: it starts the gateway, injects +//! ALIEN_AI_GATEWAY_URL, and `exec`s the app so the app runs as the main process. +//! +//! Modes: +//! alien-ai-gateway --gateway-serve run the gateway forever (fixed port) +//! alien-ai-gateway -- [args...] bootstrap, then exec + +use std::net::{Ipv4Addr, SocketAddr}; +use std::os::unix::process::CommandExt; +use std::process::{Command, Stdio}; + +const DEFAULT_PORT: u16 = 9008; +const PORT_ENV: &str = "ALIEN_AI_GATEWAY_PORT"; +const URL_ENV: &str = "ALIEN_AI_GATEWAY_URL"; +const SERVE_FLAG: &str = "--gateway-serve"; + +fn port() -> u16 { + match std::env::var(PORT_ENV) { + Err(_) => DEFAULT_PORT, + // Set but unparseable: fail loud rather than silently binding a different port. + Ok(v) => v + .parse() + .unwrap_or_else(|_| die(&format!("{PORT_ENV}='{v}' is not a valid port"))), + } +} + +fn die(msg: &str) -> ! { + eprintln!("alien-ai-gateway: {msg}"); + std::process::exit(1); +} + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.get(1).map(String::as_str) == Some(SERVE_FLAG) { + serve_forever(); + } + + let app_cmd = match args.iter().position(|a| a == "--") { + Some(i) if i + 1 < args.len() => &args[i + 1..], + _ => die("usage: alien-ai-gateway -- [args...]"), + }; + + let bindings = alien_gateway::bindings_from_env() + .unwrap_or_else(|e| die(&format!("could not read the AI bindings: {e}"))); + if bindings.is_empty() { + // No ambient AI binding (or BYO-key only): run the app directly, zero overhead. + exec_app(app_cmd); + } + + // exec_app replaces this process image, so the gateway can't run in-process — it + // wouldn't survive the exec. Spawn a separate copy of ourselves to serve it, wait + // until it's ready, then exec the app. + let self_exe = + std::env::current_exe().unwrap_or_else(|e| die(&format!("cannot resolve own path: {e}"))); + let mut child = Command::new(self_exe) + .arg(SERVE_FLAG) + .stdin(Stdio::null()) + .spawn() + .unwrap_or_else(|e| die(&format!("failed to start gateway child: {e}"))); + + let base = format!("http://127.0.0.1:{}", port()); + if !alien_gateway::wait_until_ready_blocking(&base) { + // Surface the child's own startup failure (e.g. an unavailable ambient + // credential) rather than only a generic readiness timeout. + if let Ok(Some(status)) = child.try_wait() { + die(&format!("gateway process exited before ready ({status})")); + } + die(&format!("gateway at {base} did not become ready")); + } + std::env::set_var(URL_ENV, &base); + exec_app(app_cmd); +} + +fn serve_forever() -> ! { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap_or_else(|e| die(&format!("tokio runtime: {e}"))); + rt.block_on(async { + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port())); + let bindings = alien_gateway::bindings_from_env() + .unwrap_or_else(|e| die(&format!("could not read the AI bindings: {e}"))); + let _handle = alien_gateway::start_gateway_on(bindings, addr) + .await + .unwrap_or_else(|e| die(&format!("gateway failed to start: {e}"))); + // Hold the process (and the server task) open for the container lifetime. + std::future::pending::<()>().await; + }); + unreachable!() +} + +fn exec_app(app_cmd: &[String]) -> ! { + let err = Command::new(&app_cmd[0]).args(&app_cmd[1..]).exec(); + die(&format!("failed to exec {}: {err}", app_cmd[0])); +} diff --git a/crates/alien-gateway/src/config.rs b/crates/alien-gateway/src/config.rs new file mode 100644 index 000000000..c4411ea23 --- /dev/null +++ b/crates/alien-gateway/src/config.rs @@ -0,0 +1,382 @@ +//! Build the gateway's routing table from the bindings the runtime injects. +//! +//! Each `ai` resource the workload links is injected as `ALIEN__BINDING` JSON. +//! That env-var namespace is shared with every other resource type, so we parse each +//! value as an `AiBinding` and keep only the ones that match (a non-AI binding carries +//! no matching AI `service` tag, so it fails to parse). `resolve_route` then attaches +//! the cloud's ambient credential to produce a route the proxy can serve. + +use std::collections::HashMap; +use std::sync::Arc; + +use alien_bindings::provider::LazyEnvBindingsProvider; +use alien_bindings::BindingsProvider; +use alien_core::bindings::AiBinding; +use alien_core::{ + AzureCredentials, ClientConfig, GcpCredentials, Platform, ENV_ALIEN_DEPLOYMENT_TOKEN, + ENV_ALIEN_DEPLOYMENT_TYPE, ENV_ALIEN_MANAGER_URL, +}; +use alien_error::{AlienError, Context, IntoAlienError}; + +use crate::creds::{AmbientCred, AwsSigV4Cred, BearerTokenCred}; +use crate::error::{ErrorData, Result}; +use crate::{GatewayBinding, GatewayRoute, TunedRoute}; + +/// The shared workload credential resolver used for the mint-gated (runtime-less) path. +pub type Managed = Arc; + +/// Build the shared credential resolver once, only when the runtime-less mint gate is present +/// (`ALIEN_MANAGER_URL` + `ALIEN_DEPLOYMENT_TOKEN`). The gateway holds it for the process so +/// every request re-resolves through it and short-lived minted credentials refresh before +/// they expire. Absent (a projected-identity workload) → `None`, and routes fall back to the +/// self-refreshing native credential path. +pub fn managed_provider() -> Result> { + let env: HashMap = std::env::vars().collect(); + let mint_gate = env.contains_key(ENV_ALIEN_MANAGER_URL) && env.contains_key(ENV_ALIEN_DEPLOYMENT_TOKEN); + if !mint_gate { + return Ok(None); + } + // Past the gate, `from_env_lazy` only fails on an unusable deployment type or malformed + // binding JSON. Swallowing that would sign upstream calls with whatever ambient identity + // the host happens to carry, instead of the workload's. + let provider = BindingsProvider::from_env_lazy(env).context(ErrorData::Other { + message: "the workload credential resolver could not be built".to_string(), + })?; + Ok(Some(Arc::new(provider))) +} + +/// AAD audience for an Azure AI Services / Foundry account token. +const AZURE_AI_AUDIENCE: &str = "https://cognitiveservices.azure.com"; + +/// The workload's resolved cloud credentials, from the runtime-less credential resolver: the +/// native/projected identity when present, else Alien-minted short-lived credentials (the +/// resolver in `alien-bindings` mints against the manager when `ALIEN_MANAGER_URL` + +/// `ALIEN_DEPLOYMENT_TOKEN` are set). `None` when neither is available (e.g. a projected +/// instance-role container that carries no explicit config), so the caller falls back to the +/// SDK default chain / metadata server, which resolve the same projected identity. +async fn workload_client_config() -> Result> { + let env: HashMap = std::env::vars().collect(); + // No deployment type: the gateway is running outside an Alien workload (local dev, the + // standalone launcher), so there is no resolved identity and the caller falls back to the + // SDK default chain / metadata server. Any other failure is real. + if !env.contains_key(ENV_ALIEN_DEPLOYMENT_TYPE) { + return Ok(None); + } + let lazy = BindingsProvider::from_env_lazy(env).context(ErrorData::Other { + message: "the workload credential resolver could not be built".to_string(), + })?; + let provider = lazy.provider().await.context(ErrorData::AmbientCredentialUnavailable { + message: "the workload credential resolver failed".to_string(), + })?; + Ok(Some(provider.client_config().clone())) +} + +/// The workload's GCP access token when the resolver produced a ready OAuth2 token (the minted / +/// explicit case). `None` for any other variant, so the caller falls back to the metadata server. +async fn gcp_access_token() -> Result> { + let Some(config) = workload_client_config().await? else { + return Ok(None); + }; + Ok(config.gcp_config().and_then(|g| match &g.credentials { + GcpCredentials::AccessToken { token } => Some(token.clone()), + _ => None, + })) +} + +/// The workload's Azure access token when the resolver produced a ready bearer token; `None` +/// otherwise, so the caller falls back to Azure IMDS (the projected-identity case). +async fn azure_access_token() -> Result> { + let Some(config) = workload_client_config().await? else { + return Ok(None); + }; + Ok(config.azure_config().and_then(|a| match &a.credentials { + AzureCredentials::AccessToken { token } => Some(token.clone()), + _ => None, + })) +} + +/// Parse each `ALIEN__BINDING` env var into a `GatewayBinding`, skipping non-AI +/// bindings and the External BYO-key variant. +pub fn bindings_from_env() -> Result> { + bindings_from_pairs(std::env::vars()) +} + +/// Like [`bindings_from_env`], but over an explicit env map instead of `std::env::vars`. +pub fn bindings_from_env_map(env: &HashMap) -> Result> { + bindings_from_pairs(env.iter().map(|(k, v)| (k.clone(), v.clone()))) +} + +/// The `service` tags `AiBinding` deserializes. A binding carrying any other tag belongs to +/// another resource type sharing the `ALIEN__BINDING` namespace. +const AI_SERVICE_TAGS: [&str; 4] = ["bedrock", "vertex", "foundry", "external"]; + +#[derive(serde::Deserialize)] +struct ServiceTag { + service: String, +} + +/// The binding's canonical name — the same decode `alien-bindings` applies, so the gateway's +/// route keys match the resolver's binding-map keys and a caller can append the resource's +/// own id (`//v1`). +fn canonical_binding_name(env_key_name: &str) -> String { + env_key_name.to_lowercase().replace('_', "-") +} + +fn bindings_from_pairs( + pairs: impl Iterator, +) -> Result> { + let mut bindings = Vec::new(); + for (key, value) in pairs { + let Some(name) = key.strip_prefix("ALIEN_").and_then(|k| k.strip_suffix("_BINDING")) else { + continue; + }; + // Not an AI binding: another resource type in the shared namespace. + let Ok(tag) = serde_json::from_str::(&value) else { + continue; + }; + if !AI_SERVICE_TAGS.contains(&tag.service.as_str()) { + continue; + } + let name = canonical_binding_name(name); + // Ours, but unparseable. Skipping would drop the route and leave the app calling a + // gateway that serves nothing, so surface it here. + let binding: AiBinding = serde_json::from_str(&value).into_alien_error().context( + ErrorData::BindingConfigInvalid { + binding: name.clone(), + message: format!("the '{}' binding could not be parsed", tag.service), + }, + )?; + if let Some(binding) = gateway_binding(&name, binding) { + bindings.push(binding); + } + } + Ok(bindings) +} + +/// Map a parsed `AiBinding` to a `GatewayBinding`, or `None` for variants the gateway +/// does not serve. The binding name is the canonical path segment (lowercased, as the +/// env-var key encodes it). +fn gateway_binding(name: &str, binding: AiBinding) -> Option { + // A managed binding may carry a tuned model the gateway serves alongside the + // static catalog, and/or a fine-tuning capability the control-plane routes use. + // Read both before consuming the binding into its variant. + let tuned = binding.tuned_model().map(|t| TunedRoute { + served_id: t.served_id.clone(), + upstream_id: t.upstream_id.clone(), + }); + let finetune = binding.finetune().cloned(); + match binding { + AiBinding::Bedrock(b) => Some(GatewayBinding { + name: name.to_string(), + cloud: Platform::Aws, + region: Some(b.region), + project: None, + azure_endpoint: None, + tuned, + finetune, + }), + AiBinding::Vertex(b) => Some(GatewayBinding { + name: name.to_string(), + cloud: Platform::Gcp, + region: Some(b.location), + project: Some(b.project), + azure_endpoint: None, + tuned, + finetune, + }), + AiBinding::Foundry(b) => Some(GatewayBinding { + name: name.to_string(), + cloud: Platform::Azure, + region: None, + project: None, + azure_endpoint: Some(b.endpoint), + tuned, + finetune, + }), + // External is a BYO-key provider, not an ambient-managed cloud — not served here. + AiBinding::External(_) => None, + } +} + +/// Attach the binding's cloud ambient credential, producing a route the proxy serves. +pub async fn resolve_route(binding: GatewayBinding, managed: Option<&Managed>) -> Result { + // The gateway authorizes upstream calls with the workload's own identity. Both paths + // handle either shape the resolver can select: minted short-lived credentials, which each + // request re-resolves so they refresh before expiry, or the workload's projected identity, + // which the SDK default chain / metadata server resolves and self-refreshes. An explicitly + // supplied token is used as-is — it carries no refresh clock. + let cred = match binding.cloud { + Platform::Aws => { + let region = binding.region.clone().ok_or_else(|| { + AlienError::new(ErrorData::BindingConfigInvalid { + binding: binding.name.clone(), + message: "an AWS binding needs a region".to_string(), + }) + })?; + match managed { + Some(p) => AmbientCred::Aws(AwsSigV4Cred::managed(region, p.clone())), + None => match workload_client_config().await? { + Some(config) if config.aws_config().is_some() => { + AmbientCred::Aws(AwsSigV4Cred::from_client_config(region, &config).await?) + } + _ => AmbientCred::Aws(AwsSigV4Cred::new(region).await?), + }, + } + } + Platform::Gcp => match managed { + Some(p) => AmbientCred::Bearer(BearerTokenCred::managed_gcp(p.clone())), + None => match gcp_access_token().await? { + Some(token) => AmbientCred::Bearer(BearerTokenCred::static_token(token)), + None => AmbientCred::Bearer(BearerTokenCred::gcp()), + }, + }, + Platform::Azure => match managed { + Some(p) => AmbientCred::Bearer(BearerTokenCred::managed_azure(p.clone(), AZURE_AI_AUDIENCE)), + None => match azure_access_token().await? { + Some(token) => AmbientCred::Bearer(BearerTokenCred::static_token(token)), + None => AmbientCred::Bearer(BearerTokenCred::azure(AZURE_AI_AUDIENCE)), + }, + }, + other => { + return Err(AlienError::new(ErrorData::BindingConfigInvalid { + binding: binding.name.clone(), + message: format!("the AI gateway does not serve {other:?} bindings"), + })) + } + }; + + Ok(GatewayRoute { + name: binding.name, + cloud: binding.cloud, + region: binding.region, + project: binding.project, + azure_endpoint: binding.azure_endpoint, + cred, + upstream_base_override: None, + tuned: binding.tuned, + finetune: binding.finetune, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_each_managed_binding_to_its_cloud() { + let aws = gateway_binding("llm", AiBinding::bedrock("us-east-2")).unwrap(); + assert_eq!(aws.cloud, Platform::Aws); + assert_eq!(aws.region.as_deref(), Some("us-east-2")); + + let gcp = gateway_binding("llm", AiBinding::vertex("proj", "us-central1")).unwrap(); + assert_eq!(gcp.cloud, Platform::Gcp); + assert_eq!(gcp.region.as_deref(), Some("us-central1")); + assert_eq!(gcp.project.as_deref(), Some("proj")); + + let azure = + gateway_binding("llm", AiBinding::foundry("https://x.openai.azure.com/", "acct")).unwrap(); + assert_eq!(azure.cloud, Platform::Azure); + assert_eq!(azure.azure_endpoint.as_deref(), Some("https://x.openai.azure.com/")); + } + + #[test] + fn skips_external_byo_key_binding() { + assert!(gateway_binding("llm", AiBinding::external("openai", "sk-x")).is_none()); + } + + #[test] + fn parses_an_ai_binding_from_the_env_var() { + temp_env::with_var( + "ALIEN_LLM_BINDING", + Some(r#"{"service":"foundry","endpoint":"https://x.openai.azure.com/","account":"x"}"#), + || { + let binding = bindings_from_env() + .expect("the env holds a valid AI binding") + .into_iter() + .find(|b| b.name == "llm") + .expect("the llm binding should be parsed from the env"); + assert_eq!(binding.cloud, Platform::Azure); + assert_eq!(binding.azure_endpoint.as_deref(), Some("https://x.openai.azure.com/")); + }, + ); + } + + /// The route key is the resource's own id, so a caller appends `//v1` without + /// knowing how the env var encoded it. + #[test] + fn the_route_key_is_the_canonical_binding_name() { + temp_env::with_var( + "ALIEN_MY_LLM_BINDING", + Some(r#"{"service":"bedrock","region":"us-east-2"}"#), + || { + let bindings = bindings_from_env().expect("the env holds a valid AI binding"); + assert_eq!(bindings.iter().map(|b| b.name.as_str()).collect::>(), ["my-llm"]); + }, + ); + } + + #[test] + fn ignores_non_ai_bindings_in_the_shared_namespace() { + temp_env::with_var( + "ALIEN_DB_BINDING", + Some(r#"{"connectionUrl":"postgres://localhost/db"}"#), + || { + let bindings = bindings_from_env().expect("a non-AI binding is not an error"); + assert!( + !bindings.iter().any(|b| b.name == "db"), + "a non-AI binding must not be picked up by the gateway" + ); + }, + ); + } + + /// Another resource type's binding carries its own `service` tag; that is not ours. + #[test] + fn ignores_a_non_ai_service_tag() { + temp_env::with_var( + "ALIEN_DB_BINDING", + Some(r#"{"service":"aurora","clusterEndpoint":"x","port":5432}"#), + || { + let bindings = bindings_from_env().expect("a non-AI service tag is not an error"); + assert!(bindings.is_empty(), "a postgres binding must not be picked up"); + }, + ); + } + + /// Skipping a malformed AI binding would drop its route and leave the app calling a + /// gateway that serves nothing, so it must fail loudly instead. + #[test] + fn a_malformed_ai_binding_is_an_error() { + temp_env::with_var("ALIEN_LLM_BINDING", Some(r#"{"service":"bedrock"}"#), || { + let err = bindings_from_env().expect_err("a bedrock binding with no region must fail"); + assert_eq!(err.code, "GATEWAY_BINDING_CONFIG_INVALID"); + }); + } + + /// `AI_SERVICE_TAGS` is hand-maintained, so a new `AiBinding` variant added without + /// updating it would be silently dropped as "not an AI binding". Pin the two together: + /// every variant's serialized `service` tag must be listed, and nothing extra. + #[test] + fn ai_service_tags_match_the_binding_variants() { + let tag_of = |b: &AiBinding| { + serde_json::to_value(b).unwrap()["service"] + .as_str() + .unwrap() + .to_string() + }; + let mut from_variants = [ + tag_of(&AiBinding::bedrock("us-east-1")), + tag_of(&AiBinding::vertex("p", "us-central1")), + tag_of(&AiBinding::foundry("https://x", "a")), + tag_of(&AiBinding::external("openai", "sk-x")), + ]; + from_variants.sort_unstable(); + let mut declared = AI_SERVICE_TAGS.map(str::to_string); + declared.sort_unstable(); + assert_eq!( + from_variants.as_slice(), + declared.as_slice(), + "AI_SERVICE_TAGS drifted from AiBinding's variants; update the array" + ); + } +} diff --git a/crates/alien-gateway/src/creds.rs b/crates/alien-gateway/src/creds.rs new file mode 100644 index 000000000..7e1a0fe44 --- /dev/null +++ b/crates/alien-gateway/src/creds.rs @@ -0,0 +1,515 @@ +//! Per-cloud ambient credential injection. +//! +//! Each provider authorizes an outgoing upstream request with the workload's +//! ambient identity and never reads a static key from the binding or env: +//! - AWS: SigV4-sign using the SDK default chain (env / profile / SSO / IMDS / +//! IRSA), which on a workload is the instance role. The signing service name is +//! per-request: `bedrock` for the OpenAI-compatible endpoint, `bedrock-mantle` +//! for the Anthropic Messages endpoint. +//! - GCP / Azure: attach a bearer token fetched from the instance metadata service +//! (GCE metadata server / Azure IMDS), cached until shortly before it expires. + +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use alien_bindings::provider::LazyEnvBindingsProvider; +use alien_core::{AwsCredentials, AzureCredentials, ClientConfig, GcpCredentials}; +use alien_error::{AlienError, Context, IntoAlienError}; +use aws_credential_types::provider::error::CredentialsError; +use aws_credential_types::provider::{future, ProvideCredentials, SharedCredentialsProvider}; +use aws_credential_types::Credentials; +use aws_sigv4::http_request::{sign, SignableBody, SignableRequest, SigningSettings}; +use aws_sigv4::sign::v4; +use http::{HeaderName, HeaderValue}; +use tokio::sync::Mutex; + +use crate::error::{ErrorData, Result}; + +/// The shared workload credential resolver, resolved fresh on each authorization so a +/// long-lived gateway re-mints short-lived credentials before they expire. Held behind an +/// `Arc` because every route the mint-gated workload serves shares one provider (one mint +/// cache, one refresh clock). +type Managed = Arc; + +/// AWS credentials provider backed by the runtime-less resolver. The SigV4 signer calls +/// `provide_credentials` on every request, and `LazyEnvBindingsProvider::provider` re-mints +/// only when the cached credential is near expiry, so this refreshes transparently. +/// +/// The resolver is native-first: it yields minted keys only when the workload has no +/// projected identity. So both shapes reach here, and an `Imds` / `Profile` / `WebIdentity` +/// config is resolved through the SDK default chain — the same fallback +/// [`AwsSigV4Cred::from_client_config`] uses. Rejecting it would fail every request on a +/// mint-gated workload that has a projected identity. +#[derive(Debug)] +struct ManagedAwsCredentials { + provider: Managed, + /// The SDK default chain, built once on the first projected-identity request. + native: tokio::sync::OnceCell, +} + +impl ManagedAwsCredentials { + fn new(provider: Managed) -> Self { + Self { provider, native: tokio::sync::OnceCell::new() } + } + + /// The SDK default chain, which resolves and refreshes the workload's projected identity. + async fn native_chain(&self) -> std::result::Result<&SharedCredentialsProvider, CredentialsError> { + self.native + .get_or_try_init(|| async { + let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; + config.credentials_provider().ok_or_else(|| { + CredentialsError::not_loaded("the AWS default credential chain provided no credentials provider") + }) + }) + .await + } + + async fn resolve(&self) -> std::result::Result { + let provider = self.provider.provider().await.map_err(CredentialsError::provider_error)?; + let aws = provider + .client_config() + .aws_config() + .ok_or_else(|| CredentialsError::not_loaded("the workload identity is not an AWS credential"))?; + match &aws.credentials { + AwsCredentials::AccessKeys { access_key_id, secret_access_key, session_token } => Ok( + Credentials::new(access_key_id, secret_access_key, session_token.clone(), None, "alien-managed"), + ), + AwsCredentials::SessionCredentials { access_key_id, secret_access_key, session_token, .. } => Ok( + Credentials::new(access_key_id, secret_access_key, Some(session_token.clone()), None, "alien-managed"), + ), + // The resolver selected the workload's projected identity (Imds / Profile / + // WebIdentity); the SDK default chain resolves and refreshes it. + _ => self.native_chain().await?.provide_credentials().await, + } + } +} + +impl ProvideCredentials for ManagedAwsCredentials { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::new(self.resolve()) + } +} + +/// Injects the workload's ambient identity into an outgoing upstream request. +pub enum AmbientCred { + Aws(AwsSigV4Cred), + Bearer(BearerTokenCred), +} + +impl AmbientCred { + /// Authorize an outgoing upstream request. `aws_sigv4_service` is the SigV4 + /// service name (`bedrock` or `bedrock-mantle`); it is consumed only by the AWS + /// variant and ignored by bearer-token clouds. + pub async fn authorize(&self, req: &mut reqwest::Request, aws_sigv4_service: &str) -> Result<()> { + match self { + AmbientCred::Aws(c) => c.sign(req, aws_sigv4_service).await, + AmbientCred::Bearer(c) => c.attach(req).await, + } + } +} + +/// SigV4 signer for AWS Bedrock using the SDK default credential chain. +pub struct AwsSigV4Cred { + region: String, + provider: SharedCredentialsProvider, +} + +impl AwsSigV4Cred { + /// Resolve credentials from the SDK default chain (workload instance role in + /// production; env / profile / SSO locally). + pub async fn new(region: impl Into) -> Result { + let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; + let provider = config.credentials_provider().ok_or_else(|| { + AlienError::new(ErrorData::WorkloadIdentityInvalid { + message: "the AWS default credential chain provided no credentials provider".to_string(), + }) + })?; + Ok(Self { region: region.into(), provider }) + } + + /// Build from an explicit credentials provider (used by tests). + pub fn with_provider(region: impl Into, provider: SharedCredentialsProvider) -> Self { + Self { region: region.into(), provider } + } + + /// Sign with credentials the runtime-less resolver mints and refreshes. Each request + /// re-resolves through the shared provider, so minted credentials never go stale. + pub fn managed(region: impl Into, provider: Managed) -> Self { + Self { + region: region.into(), + provider: SharedCredentialsProvider::new(ManagedAwsCredentials::new(provider)), + } + } + + /// Build from the workload's resolved `ClientConfig` — the identity the runtime-less + /// credential resolver selected: the native/projected identity, or Alien-minted + /// short-lived credentials. Explicit keys/session credentials are signed directly; + /// an instance-role/profile config falls back to the SDK default chain, which + /// resolves (and refreshes) them. + pub async fn from_client_config(region: impl Into, config: &ClientConfig) -> Result { + let region = region.into(); + let aws = config.aws_config().ok_or_else(|| { + AlienError::new(ErrorData::WorkloadIdentityInvalid { + message: "the workload ClientConfig is not an AWS config".to_string(), + }) + })?; + match &aws.credentials { + AwsCredentials::AccessKeys { + access_key_id, + secret_access_key, + session_token, + } => { + let creds = Credentials::new( + access_key_id, + secret_access_key, + session_token.clone(), + None, + "alien-workload", + ); + Ok(Self::with_provider(region, SharedCredentialsProvider::new(creds))) + } + AwsCredentials::SessionCredentials { + access_key_id, + secret_access_key, + session_token, + .. + } => { + let creds = Credentials::new( + access_key_id, + secret_access_key, + Some(session_token.clone()), + None, + "alien-workload", + ); + Ok(Self::with_provider(region, SharedCredentialsProvider::new(creds))) + } + // Imds / Profile: the SDK default chain resolves the projected identity. + _ => Self::new(region).await, + } + } + + async fn sign(&self, req: &mut reqwest::Request, service: &str) -> Result<()> { + let creds = self + .provider + .provide_credentials() + .await + .into_alien_error() + .context(ErrorData::AmbientCredentialUnavailable { + message: format!("the SigV4 signer for {service} got no credentials from the provider"), + })?; + let identity = creds.into(); + let params = v4::SigningParams::builder() + .identity(&identity) + .region(&self.region) + .name(service) + .time(std::time::SystemTime::now()) + .settings(SigningSettings::default()) + .build() + .into_alien_error() + .context(ErrorData::Other { message: "could not build SigV4 params".to_string() })?; + + // SigV4 hashes the exact body; a non-in-memory (streaming) body would sign as + // empty and silently fail upstream, so reject it rather than mis-sign. + let body_bytes: Vec = match req.body() { + Some(body) => body + .as_bytes() + .ok_or_else(|| { + AlienError::new(ErrorData::Other { + message: "cannot SigV4-sign a streaming (non-in-memory) request body".to_string(), + }) + })? + .to_vec(), + None => Vec::new(), + }; + let uri = req.url().to_string(); + let mut headers: Vec<(String, String)> = req + .headers() + .iter() + .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str().to_string(), v.to_string()))) + .collect(); + // SigV4 must cover the Host header; reqwest sets it from the URL at send time, + // so sign the same value (with port when non-default) for the signature to match. + if let Some(host) = req.url().host_str() { + let host_header = match req.url().port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + headers.push(("host".to_string(), host_header)); + } + let signable = SignableRequest::new( + req.method().as_str(), + &uri, + headers.iter().map(|(k, v)| (k.as_str(), v.as_str())), + SignableBody::Bytes(&body_bytes), + ) + .into_alien_error() + .context(ErrorData::Other { message: "could not build signable request".to_string() })?; + + let (instructions, _signature) = sign(signable, ¶ms.into()) + .into_alien_error() + .context(ErrorData::Other { message: "SigV4 signing failed".to_string() })? + .into_parts(); + + for (name, value) in instructions.headers() { + let hn = HeaderName::from_bytes(name.as_bytes()) + .into_alien_error() + .context(ErrorData::Other { message: format!("invalid signed header name {name}") })?; + let hv = HeaderValue::from_str(value) + .into_alien_error() + .context(ErrorData::Other { message: "invalid signed header value".to_string() })?; + req.headers_mut().insert(hn, hv); + } + Ok(()) + } +} + +/// Where a bearer token comes from: the GCE metadata server, Azure IMDS, or a +/// pre-supplied token (local-dev ADC / `az` CLI token, or tests). +enum BearerSource { + Gcp, + Azure { resource: String }, + Static { token: String }, + /// The runtime-less resolver: re-resolve the workload's bearer token on each request, + /// re-minting when the short-lived credential is near expiry. The resolver is + /// native-first, so it may instead select the workload's projected identity, which + /// carries no bearer token — `native` is the metadata source that issues one. + Managed { provider: Managed, native: Box }, +} + +/// Attaches an ambient bearer token — fetched and cached from the cloud metadata endpoint, +/// or supplied directly (static / minted). +pub struct BearerTokenCred { + source: BearerSource, + client: reqwest::Client, + cache: Mutex>, +} + +impl BearerTokenCred { + pub fn gcp() -> Self { + Self { source: BearerSource::Gcp, client: reqwest::Client::new(), cache: Mutex::new(None) } + } + + /// `resource` is the audience, e.g. `https://cognitiveservices.azure.com`. + pub fn azure(resource: impl Into) -> Self { + Self { + source: BearerSource::Azure { resource: resource.into() }, + client: reqwest::Client::new(), + cache: Mutex::new(None), + } + } + + /// Use a pre-supplied bearer token instead of the metadata service — for + /// local development (an ADC / `az` CLI token) or tests. + pub fn static_token(token: impl Into) -> Self { + Self { + source: BearerSource::Static { token: token.into() }, + client: reqwest::Client::new(), + cache: Mutex::new(None), + } + } + + /// Resolve the GCP bearer token from the runtime-less resolver, falling back to the + /// metadata service when the resolver selects the workload's projected identity. + pub fn managed_gcp(provider: Managed) -> Self { + Self::managed(provider, BearerSource::Gcp) + } + + /// Resolve the Azure bearer token from the runtime-less resolver, falling back to IMDS + /// for `resource` when the resolver selects the workload's projected identity. + pub fn managed_azure(provider: Managed, resource: impl Into) -> Self { + Self::managed(provider, BearerSource::Azure { resource: resource.into() }) + } + + fn managed(provider: Managed, native: BearerSource) -> Self { + Self { + source: BearerSource::Managed { provider, native: Box::new(native) }, + client: reqwest::Client::new(), + cache: Mutex::new(None), + } + } + + async fn attach(&self, req: &mut reqwest::Request) -> Result<()> { + let token = self.token().await?; + let hv = HeaderValue::from_str(&format!("Bearer {token}")) + .into_alien_error() + .context(ErrorData::Other { message: "invalid bearer token".to_string() })?; + req.headers_mut().insert(http::header::AUTHORIZATION, hv); + Ok(()) + } + + async fn token(&self) -> Result { + match &self.source { + // A supplied token has no refresh clock of its own, and the resolver holds one + // for minted credentials, so neither uses the local metadata cache. + BearerSource::Static { token } => Ok(token.clone()), + BearerSource::Managed { provider, native } => match self.managed_token(provider, native).await? { + Some(token) => Ok(token), + None => self.metadata_token(native).await, + }, + native => self.metadata_token(native).await, + } + } + + /// The bearer token the resolver selected, or `None` when it selected the workload's + /// projected identity — which carries no token of its own, so the metadata service + /// issues one. + async fn managed_token(&self, provider: &Managed, native: &BearerSource) -> Result> { + let resolved = provider.provider().await.context(ErrorData::AmbientCredentialUnavailable { + message: "the workload credential resolver failed".to_string(), + })?; + Ok(match native { + BearerSource::Gcp => resolved.client_config().gcp_config().and_then(|g| match &g.credentials { + GcpCredentials::AccessToken { token } => Some(token.clone()), + _ => None, + }), + BearerSource::Azure { .. } => resolved.client_config().azure_config().and_then(|a| match &a.credentials { + AzureCredentials::AccessToken { token } => Some(token.clone()), + _ => None, + }), + // `managed` only ever builds a Gcp / Azure native source. + BearerSource::Static { .. } | BearerSource::Managed { .. } => None, + }) + } + + /// Cache-then-fetch the workload's projected-identity token from the instance metadata + /// service. `source` must be `Gcp` or `Azure`. + async fn metadata_token(&self, source: &BearerSource) -> Result { + if let Some((tok, exp)) = self.cache.lock().await.as_ref() { + if Instant::now() < *exp { + return Ok(tok.clone()); + } + } + + let (url, header_name, header_value) = match source { + BearerSource::Gcp => ( + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token".to_string(), + "Metadata-Flavor", + "Google".to_string(), + ), + BearerSource::Azure { resource } => ( + format!("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource={resource}"), + "Metadata", + "true".to_string(), + ), + BearerSource::Static { .. } | BearerSource::Managed { .. } => { + return Err(AlienError::new(ErrorData::Other { + message: "no metadata endpoint for this credential source".to_string(), + })) + } + }; + + let resp = self + .client + .get(&url) + .header(header_name, header_value) + .send() + .await + .into_alien_error() + .context(ErrorData::AmbientCredentialUnavailable { + message: "could not reach the instance metadata service".to_string(), + })?; + if !resp.status().is_success() { + return Err(AlienError::new(ErrorData::AmbientCredentialUnavailable { + message: format!("metadata token endpoint returned {}", resp.status()), + })); + } + // GCP returns expires_in as a number; Azure IMDS returns it as a string. + let v: serde_json::Value = resp + .json() + .await + .into_alien_error() + .context(ErrorData::Other { message: "could not parse the metadata token response".to_string() })?; + let access_token = v["access_token"] + .as_str() + .ok_or_else(|| AlienError::new(ErrorData::Other { + message: "metadata token response had no access_token".to_string(), + }))? + .to_string(); + // A wrong shape here would silently cache a token past its real expiry, and there is + // no 401-triggered invalidation to recover — so fail rather than invent a lifetime. + let expires_in = v["expires_in"] + .as_u64() + .or_else(|| v["expires_in"].as_str().and_then(|s| s.parse().ok())) + .ok_or_else(|| { + AlienError::new(ErrorData::AmbientCredentialUnavailable { + message: "metadata token response had no usable expires_in".to_string(), + }) + })?; + + let exp = Instant::now() + Duration::from_secs(expires_in.saturating_sub(60)); + *self.cache.lock().await = Some((access_token.clone(), exp)); + Ok(access_token) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn aws_signer_adds_sigv4_headers() { + // Hermetic: sign with explicit static creds (no env / network needed). + let creds = aws_credential_types::Credentials::new( + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + None, + None, + "test", + ); + let cred = AwsSigV4Cred::with_provider("us-east-2", SharedCredentialsProvider::new(creds)); + + let mut req = reqwest::Client::new() + .post("https://bedrock-runtime.us-east-2.amazonaws.com/openai/v1/chat/completions") + .body(r#"{"model":"openai.gpt-oss-20b-1:0","messages":[]}"#) + .build() + .expect("request builds"); + + cred.sign(&mut req, "bedrock").await.expect("signing succeeds"); + + let auth = req + .headers() + .get(http::header::AUTHORIZATION) + .expect("authorization header present") + .to_str() + .unwrap(); + assert!(auth.contains("AWS4-HMAC-SHA256"), "must be a SigV4 auth header: {auth}"); + assert!(auth.contains("/bedrock/"), "credential scope must name the bedrock service: {auth}"); + assert!(req.headers().contains_key("x-amz-date"), "must add x-amz-date"); + } + + #[tokio::test] + async fn aws_signer_signs_classic_invoke_stream_url() { + // Claude routes through classic InvokeModel; the signer must handle the + // profile id in the URL path and sign it under the same `bedrock` service. + let creds = aws_credential_types::Credentials::new( + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + None, + None, + "test", + ); + let cred = AwsSigV4Cred::with_provider("us-east-2", SharedCredentialsProvider::new(creds)); + + let mut req = reqwest::Client::new() + .post("https://bedrock-runtime.us-east-2.amazonaws.com/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .body(r#"{"anthropic_version":"bedrock-2023-05-31","messages":[]}"#) + .build() + .expect("request builds"); + + cred.sign(&mut req, "bedrock").await.expect("signing succeeds"); + + let auth = req + .headers() + .get(http::header::AUTHORIZATION) + .expect("authorization header present") + .to_str() + .unwrap(); + assert!( + auth.contains("/bedrock/"), + "credential scope must name the bedrock service: {auth}" + ); + } +} diff --git a/crates/alien-gateway/src/error.rs b/crates/alien-gateway/src/error.rs new file mode 100644 index 000000000..12f1536d4 --- /dev/null +++ b/crates/alien-gateway/src/error.rs @@ -0,0 +1,102 @@ +use alien_error::AlienErrorData; +use serde::{Deserialize, Serialize}; + +/// Errors raised while starting or serving the embedded AI gateway. +#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ErrorData { + /// The gateway could not bind its loopback listener. + #[error( + code = "GATEWAY_BIND_FAILED", + message = "Failed to bind the AI gateway on {address}", + retryable = "false", + internal = "true" + )] + BindFailed { address: String }, + + /// The request named a binding the gateway does not serve. + #[error( + code = "GATEWAY_UNKNOWN_BINDING", + message = "No AI binding named '{binding}'", + retryable = "false", + internal = "false", + http_status_code = 404 + )] + UnknownBinding { binding: String }, + + /// The request body was not a usable model request. + #[error( + code = "GATEWAY_INVALID_REQUEST", + message = "Invalid AI request: {message}", + retryable = "false", + internal = "false", + http_status_code = 400 + )] + InvalidRequest { message: String }, + + /// The requested model is not in the catalog for this binding's cloud. + #[error( + code = "GATEWAY_MODEL_NOT_AVAILABLE", + message = "Model '{model}' is not available on binding '{binding}'", + retryable = "false", + internal = "false", + http_status_code = 404 + )] + ModelNotAvailable { model: String, binding: String }, + + /// The upstream cloud endpoint could not be reached. Not internal: the 502 and + /// its message (a cloud endpoint host, never a credential) are safe to surface, + /// and `internal = true` would collapse this to a generic 500 at the client. + #[error( + code = "GATEWAY_UPSTREAM_FAILED", + message = "Upstream AI endpoint request failed: {message}", + retryable = "true", + internal = "false", + http_status_code = 502 + )] + UpstreamFailed { message: String }, + + /// The workload's ambient cloud identity could not be obtained (no instance + /// role, or the metadata service is unreachable) — transient, so retryable. + #[error( + code = "GATEWAY_AMBIENT_CREDENTIAL_UNAVAILABLE", + message = "Could not obtain the workload's ambient cloud credential: {message}", + retryable = "true", + internal = "true" + )] + AmbientCredentialUnavailable { message: String }, + + /// The workload's resolved identity cannot authorize this binding's cloud (e.g. an + /// AWS binding whose workload identity is not an AWS credential). A permanent + /// invariant failure, so retrying cannot succeed. + #[error( + code = "GATEWAY_WORKLOAD_IDENTITY_INVALID", + message = "The workload identity cannot authorize this binding: {message}", + retryable = "false", + internal = "true" + )] + WorkloadIdentityInvalid { message: String }, + + /// A binding's projected configuration is unusable (missing a required field, or + /// a cloud the gateway does not serve). User-fixable, so not internal. + #[error( + code = "GATEWAY_BINDING_CONFIG_INVALID", + message = "AI binding '{binding}' is invalid: {message}", + retryable = "false", + internal = "false", + http_status_code = 400 + )] + BindingConfigInvalid { binding: String, message: String }, + + /// Generic catch-all for unexpected, non-retryable gateway failures (build, + /// serialization, or configuration bugs). Transient cases get their own variant. + #[error( + code = "GATEWAY_ERROR", + message = "AI gateway error: {message}", + retryable = "false", + internal = "true" + )] + Other { message: String }, +} + +pub type Result = alien_error::Result; diff --git a/crates/alien-gateway/src/finetune.rs b/crates/alien-gateway/src/finetune.rs new file mode 100644 index 000000000..c8ffe6f0a --- /dev/null +++ b/crates/alien-gateway/src/finetune.rs @@ -0,0 +1,164 @@ +//! Runtime fine-tuning control plane for the AI gateway. +//! +//! Inference is a stateless proxy (see [`crate::router`]); fine-tuning adds a small +//! *control-plane* surface on the same gateway, reusing the same ambient credential. +//! The app triggers a job at runtime: +//! +//! - `POST //v1/finetune` → submit a job, returns `{ jobId, servedModel }` +//! - `GET //v1/finetune/` → poll it, returns `{ status, model? }` +//! +//! The gateway is per-process and stateless: a job started by one worker completes on +//! the cloud's side hours later, possibly after that worker is gone. So nothing about +//! job state is persisted. Instead the tuned model's cloud name is **deterministic** +//! from the binding's [`FinetuneCapability`], and the router rediscovers the completed +//! model by that name on the next inference request (see [`FineTuneProvider::resolve_served_model`]). +//! +//! Each cloud implements [`FineTuneProvider`]; the router dispatches on the route's cloud. + +use alien_core::bindings::FinetuneCapability; +use alien_error::AlienError; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::creds::AmbientCred; +use crate::error::{ErrorData, Result}; + +pub mod bedrock; +pub mod foundry; +pub mod vertex; + +/// The lifecycle status of a fine-tuning job, normalized across providers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum JobStatus { + /// The job is queued or running. + Running, + /// The job completed and the tuned model is ready to serve. + Succeeded, + /// The job failed; `message` on [`JobState`] carries the reason. + Failed, +} + +/// A submitted job's identity: the provider job id the app polls with. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JobHandle { + /// Provider job id (a Bedrock job ARN, Vertex tuning-job name, or Foundry job id). + pub job_id: String, + /// The public model id the tuned model will be served under once complete. + pub served_model: String, +} + +/// The observed state of a job, plus the tuned upstream id once it succeeds. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JobState { + pub status: JobStatus, + /// The tuned model's provider-native upstream id (custom-model ARN / tuned + /// endpoint / deployment name), set only when `status == Succeeded`. + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + /// A human-readable failure reason, set only when `status == Failed`. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +/// Everything a provider needs to submit one job: the declared capability plus an +/// optional per-request override of the training key (so an app can retrain on a +/// freshly-uploaded file without redeploying). +pub struct SubmitRequest<'a> { + pub capability: &'a FinetuneCapability, + /// Override for `capability.training_key`; `None` uses the declared key. + pub training_key: Option, +} + +impl SubmitRequest<'_> { + /// The training key this request uses (override or the capability's default). + pub fn training_key(&self) -> &str { + self.training_key + .as_deref() + .unwrap_or(&self.capability.training_key) + } +} + +/// A cloud's fine-tuning control plane. Implementations issue signed control-plane +/// calls via the ambient credential and translate provider responses into the +/// normalized [`JobStatus`]/[`JobState`] above. +#[async_trait] +pub trait FineTuneProvider: Send + Sync { + /// Submit a tuning job. Returns the job id to poll and the public served model id. + async fn submit(&self, request: &SubmitRequest<'_>) -> Result; + + /// Poll a previously-submitted job by its provider job id. + async fn status(&self, job_id: &str) -> Result; + + /// Rediscover the tuned model by its deterministic name, without a job id. + /// Returns the tuned upstream id if the model exists and is ready to serve, + /// `None` if it doesn't exist yet or is still being created. This is how a + /// stateless gateway routes `served_model_id` after the worker that ran the + /// job is gone. + async fn resolve_served_model(&self, capability: &FinetuneCapability) -> Result>; +} + +/// Build a per-request [`FineTuneProvider`] for a route's cloud. The provider borrows +/// the route's ambient credential and the shared HTTP client, so it is cheap to build +/// on each control-plane request. `None` for clouds/bindings the gateway does not +/// fine-tune (currently only Bedrock is wired; Vertex/Foundry follow the same trait). +/// The route location fields a provider needs to build control-plane URLs, plus the +/// optional test base-URL override. Borrowed from the router's `GatewayRoute`. +pub struct ProviderCtx<'a> { + pub cloud: alien_core::Platform, + /// AWS region / Vertex location. + pub region: Option<&'a str>, + /// GCP project id. + pub project: Option<&'a str>, + /// Azure Foundry account endpoint. + pub azure_endpoint: Option<&'a str>, + pub cred: &'a AmbientCred, + pub client: &'a reqwest::Client, + /// Test-only upstream base override (mirrors the inference proxy). + pub base_override: Option<&'a str>, +} + +pub fn provider_for<'a>(ctx: &ProviderCtx<'a>) -> Option> { + match ctx.cloud { + alien_core::Platform::Aws => ctx.region.map(|region| { + let provider = match ctx.base_override { + Some(base) => bedrock::BedrockFineTune::with_base_override( + region.to_string(), + ctx.cred, + ctx.client, + base.to_string(), + ), + None => bedrock::BedrockFineTune::new(region.to_string(), ctx.cred, ctx.client), + }; + Box::new(provider) as Box + }), + alien_core::Platform::Gcp => match (ctx.region, ctx.project) { + (Some(location), Some(project)) => Some(Box::new(vertex::VertexFineTune::new( + location.to_string(), + project.to_string(), + ctx.cred, + ctx.client, + ctx.base_override.map(str::to_string), + )) as Box), + _ => None, + }, + alien_core::Platform::Azure => ctx.azure_endpoint.map(|endpoint| { + Box::new(foundry::FoundryFineTune::new( + endpoint.to_string(), + ctx.cred, + ctx.client, + ctx.base_override.map(str::to_string), + )) as Box + }), + _ => None, + } +} + +/// Map a "no fine-tuning capability on this binding" condition to a gateway error. +pub(crate) fn no_capability(binding: &str) -> AlienError { + AlienError::new(ErrorData::InvalidRequest { + message: format!("binding '{binding}' has no fine-tuning capability declared"), + }) +} diff --git a/crates/alien-gateway/src/finetune/bedrock.rs b/crates/alien-gateway/src/finetune/bedrock.rs new file mode 100644 index 000000000..1f8b5530b --- /dev/null +++ b/crates/alien-gateway/src/finetune/bedrock.rs @@ -0,0 +1,232 @@ +//! AWS Bedrock fine-tuning provider. +//! +//! Submits `CreateModelCustomizationJob`, polls `GetModelCustomizationJob`, and +//! rediscovers the tuned model with `GetCustomModel` (which accepts the model +//! *name*, so the stateless gateway needs no stored ARN). All three are signed +//! with the workload's ambient SigV4 credential for the `bedrock` service against +//! the control-plane host `bedrock.{region}.amazonaws.com` (distinct from the +//! `bedrock-runtime` inference host). + +use alien_core::bindings::FinetuneCapability; +use alien_error::{Context, IntoAlienError}; +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::json; + +use crate::creds::AmbientCred; +use crate::error::{ErrorData, Result}; + +use super::{FineTuneProvider, JobHandle, JobState, JobStatus, SubmitRequest}; + +/// SigV4 service name for the Bedrock control plane. +const BEDROCK_SERVICE: &str = "bedrock"; + +pub struct BedrockFineTune<'a> { + region: String, + cred: &'a AmbientCred, + client: &'a reqwest::Client, + /// When set, control-plane requests target this base URL instead of the + /// region-derived Bedrock host. Lets tests aim the provider at a mock upstream + /// (mirrors the inference proxy's `upstream_base_override`). + base_override: Option, +} + +impl<'a> BedrockFineTune<'a> { + pub fn new(region: String, cred: &'a AmbientCred, client: &'a reqwest::Client) -> Self { + Self { region, cred, client, base_override: None } + } + + /// Build a provider aimed at an explicit base URL (test upstream). + pub fn with_base_override( + region: String, + cred: &'a AmbientCred, + client: &'a reqwest::Client, + base: String, + ) -> Self { + Self { region, cred, client, base_override: Some(base) } + } + + /// Bedrock control-plane host (not the `bedrock-runtime` inference host). + fn host(&self) -> String { + self.base_override + .clone() + .unwrap_or_else(|| format!("https://bedrock.{}.amazonaws.com", self.region)) + } + + /// Sign `req` with the ambient credential for the `bedrock` service and execute it, + /// returning the parsed JSON body on 2xx or a contextual error otherwise. + async fn send(&self, mut req: reqwest::Request) -> Result { + let url = req.url().to_string(); + self.cred.authorize(&mut req, BEDROCK_SERVICE).await?; + let resp = self + .client + .execute(req) + .await + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: format!("Bedrock control-plane request to {url} failed"), + })?; + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + if !status.is_success() { + return Err(alien_error::AlienError::new(ErrorData::UpstreamFailed { + // Bedrock returns a JSON `{message, __type}`; surface it verbatim. + message: format!("Bedrock {status} for {url}: {body}"), + })); + } + if body.is_empty() { + return Ok(serde_json::Value::Null); + } + serde_json::from_str(&body) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: format!("Bedrock returned non-JSON from {url}: {body}"), + }) + } + + fn build_json_post(&self, path: &str, body: serde_json::Value) -> Result { + self.client + .post(format!("{}{path}", self.host())) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .json(&body) + .build() + .into_alien_error() + .context(ErrorData::Other { + message: format!("could not build Bedrock POST {path}"), + }) + } + + fn build_get(&self, path: &str) -> Result { + self.client + .get(format!("{}{path}", self.host())) + .build() + .into_alien_error() + .context(ErrorData::Other { + message: format!("could not build Bedrock GET {path}"), + }) + } +} + +/// `CreateModelCustomizationJob` response — only the job ARN is needed. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateJobResponse { + job_arn: String, +} + +/// `GetModelCustomizationJob` response — status plus the produced custom-model ARN. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct GetJobResponse { + status: String, + #[serde(default)] + output_model_arn: Option, + #[serde(default)] + failure_message: Option, +} + +/// `GetCustomModel` response — status plus the model ARN for rediscovery. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct GetCustomModelResponse { + model_arn: String, + model_status: String, +} + +#[async_trait] +impl FineTuneProvider for BedrockFineTune<'_> { + async fn submit(&self, request: &SubmitRequest<'_>) -> Result { + let cap = request.capability; + let training_uri = format!("s3://{}/{}", cap.training_bucket, request.training_key()); + let output_uri = format!("s3://{}/alien-finetune-output/{}/", cap.training_bucket, cap.job_name); + + // clientRequestToken makes the submit idempotent so a retried POST /finetune + // (or a transport retry) doesn't create a second job for the same run. + let body = json!({ + "jobName": cap.job_name, + "customModelName": cap.job_name, + "roleArn": cap.role_arn, + "baseModelIdentifier": cap.base_model, + "customizationType": "FINE_TUNING", + "clientRequestToken": cap.job_name, + "trainingDataConfig": { "s3Uri": training_uri }, + "outputDataConfig": { "s3Uri": output_uri }, + }); + + let req = self.build_json_post("/model-customization-jobs", body)?; + let value = self.send(req).await?; + let parsed: CreateJobResponse = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Bedrock CreateModelCustomizationJob returned no jobArn".to_string(), + })?; + + Ok(JobHandle { + job_id: parsed.job_arn, + served_model: cap.served_model_id.clone(), + }) + } + + async fn status(&self, job_id: &str) -> Result { + // jobIdentifier can be the job ARN; percent-encode it for the path. + let encoded = urlencoding::encode(job_id); + let req = self.build_get(&format!("/model-customization-jobs/{encoded}"))?; + let value = self.send(req).await?; + let parsed: GetJobResponse = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Bedrock GetModelCustomizationJob returned an unexpected body".to_string(), + })?; + + Ok(match parsed.status.as_str() { + "Completed" => JobState { + status: JobStatus::Succeeded, + model: parsed.output_model_arn, + message: None, + }, + "Failed" | "Stopped" => JobState { + status: JobStatus::Failed, + model: None, + message: parsed.failure_message, + }, + // InProgress | Stopping | anything else -> still running. + _ => JobState { status: JobStatus::Running, model: None, message: None }, + }) + } + + async fn resolve_served_model(&self, cap: &FinetuneCapability) -> Result> { + // GetCustomModel accepts the model NAME, so the deterministic job_name (also the + // custom-model name) rediscovers the model without any stored ARN. A 404 means + // the job hasn't produced the model yet. + let encoded = urlencoding::encode(&cap.job_name); + let req = self.build_get(&format!("/custom-models/{encoded}"))?; + match self.send(req).await { + Ok(value) => { + let parsed: GetCustomModelResponse = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Bedrock GetCustomModel returned an unexpected body".to_string(), + })?; + Ok(match parsed.model_status.as_str() { + "Active" => Some(parsed.model_arn), + // Creating / Failed -> not servable yet. + _ => None, + }) + } + // A ResourceNotFound (model not created yet) is not an error for rediscovery. + Err(e) if is_not_found(&e) => Ok(None), + Err(e) => Err(e), + } + } +} + +/// True if the error is a Bedrock 404 / ResourceNotFound, which for rediscovery means +/// "the tuned model doesn't exist yet", not a real failure. +fn is_not_found(err: &alien_error::AlienError) -> bool { + match &err.error { + Some(ErrorData::UpstreamFailed { message }) => { + message.contains("404") || message.contains("ResourceNotFound") + } + _ => false, + } +} diff --git a/crates/alien-gateway/src/finetune/foundry.rs b/crates/alien-gateway/src/finetune/foundry.rs new file mode 100644 index 000000000..9f65fa08e --- /dev/null +++ b/crates/alien-gateway/src/finetune/foundry.rs @@ -0,0 +1,224 @@ +//! Azure AI Foundry fine-tuning provider (data plane). +//! +//! Submits `POST {endpoint}/openai/fine_tuning/jobs`, polls `GET .../jobs/{id}`, and +//! rediscovers the tuned model as the deployment named after `served_model_id`. All +//! calls carry the workload's ambient bearer token for the cognitive-services data +//! plane; the SigV4 service name is unused. +//! +//! Note: Foundry can import training data from Blob, but that requires the storage +//! account to allow public network access — a gotcha documented in the example README. + +use alien_core::bindings::FinetuneCapability; +use alien_error::{Context, IntoAlienError}; +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::json; + +use crate::creds::AmbientCred; +use crate::error::{ErrorData, Result}; + +use super::{FineTuneProvider, JobHandle, JobState, JobStatus, SubmitRequest}; + +/// Bearer auth ignores the SigV4 service name. +const UNUSED_SERVICE: &str = "cognitiveservices"; +/// Data-plane API version for fine-tuning + deployments. +const API_VERSION: &str = "2024-10-21"; + +pub struct FoundryFineTune<'a> { + endpoint: String, + cred: &'a AmbientCred, + client: &'a reqwest::Client, + base_override: Option, +} + +impl<'a> FoundryFineTune<'a> { + pub fn new( + endpoint: String, + cred: &'a AmbientCred, + client: &'a reqwest::Client, + base_override: Option, + ) -> Self { + Self { endpoint, cred, client, base_override } + } + + /// The account data-plane base (or the test override), without a trailing slash. + fn base(&self) -> String { + self.base_override + .clone() + .unwrap_or_else(|| self.endpoint.clone()) + .trim_end_matches('/') + .to_string() + } + + async fn send(&self, mut req: reqwest::Request) -> Result { + let url = req.url().to_string(); + self.cred.authorize(&mut req, UNUSED_SERVICE).await?; + let resp = self + .client + .execute(req) + .await + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: format!("Foundry fine-tuning request to {url} failed"), + })?; + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + if !status.is_success() { + return Err(alien_error::AlienError::new(ErrorData::UpstreamFailed { + message: format!("Foundry {status} for {url}: {body}"), + })); + } + if body.is_empty() { + return Ok(serde_json::Value::Null); + } + serde_json::from_str(&body) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: format!("Foundry returned non-JSON from {url}: {body}"), + }) + } +} + +/// A fine-tuning job — `id` to poll, `status`, and `fine_tuned_model` once done. +#[derive(Deserialize)] +struct FineTuningJob { + #[serde(default)] + id: Option, + #[serde(default)] + status: Option, + #[serde(default)] + fine_tuned_model: Option, + #[serde(default)] + error: Option, +} + +#[derive(Deserialize)] +struct JobError { + #[serde(default)] + message: Option, +} + +/// A deployment resource — `provisioningState` tells us if the tuned model is servable. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct Deployment { + #[serde(default)] + provisioning_state: Option, +} + +fn map_status(status: Option<&str>, model: Option, err: Option) -> JobState { + match status { + Some("succeeded") => JobState { + status: JobStatus::Succeeded, + model, + message: None, + }, + Some("failed") | Some("cancelled") => { + JobState { status: JobStatus::Failed, model: None, message: err } + } + _ => JobState { status: JobStatus::Running, model: None, message: None }, + } +} + +#[async_trait] +impl FineTuneProvider for FoundryFineTune<'_> { + async fn submit(&self, request: &SubmitRequest<'_>) -> Result { + let cap = request.capability; + // The training file is a Blob URL under the training bucket/container. Foundry + // requires the storage account to allow public network access for Blob import. + let training_file = format!( + "https://{}.blob.core.windows.net/{}", + cap.training_bucket, + request.training_key() + ); + let body = json!({ + "model": cap.base_model, + "training_file": training_file, + "suffix": cap.served_model_id, + }); + let url = format!("{}/openai/fine_tuning/jobs?api-version={API_VERSION}", self.base()); + let req = self + .client + .post(url) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .json(&body) + .build() + .into_alien_error() + .context(ErrorData::Other { message: "could not build Foundry fine_tuning POST".to_string() })?; + let value = self.send(req).await?; + let parsed: FineTuningJob = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Foundry CreateFineTuningJob returned no id".to_string(), + })?; + let job_id = parsed.id.ok_or_else(|| { + alien_error::AlienError::new(ErrorData::UpstreamFailed { + message: "Foundry fine-tuning job response had no id".to_string(), + }) + })?; + Ok(JobHandle { job_id, served_model: cap.served_model_id.clone() }) + } + + async fn status(&self, job_id: &str) -> Result { + let encoded = urlencoding::encode(job_id); + let url = format!( + "{}/openai/fine_tuning/jobs/{encoded}?api-version={API_VERSION}", + self.base() + ); + let req = self + .client + .get(url) + .build() + .into_alien_error() + .context(ErrorData::Other { message: "could not build Foundry GET job".to_string() })?; + let value = self.send(req).await?; + let parsed: FineTuningJob = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Foundry GetFineTuningJob returned an unexpected body".to_string(), + })?; + Ok(map_status( + parsed.status.as_deref(), + parsed.fine_tuned_model, + parsed.error.and_then(|e| e.message), + )) + } + + async fn resolve_served_model(&self, cap: &FinetuneCapability) -> Result> { + // The tuned model is served as a deployment named `served_model_id`; if it + // exists and is Succeeded, that deployment name is the OpenAI-path `model`. + let encoded = urlencoding::encode(&cap.served_model_id); + let url = format!( + "{}/openai/deployments/{encoded}?api-version={API_VERSION}", + self.base() + ); + let req = self + .client + .get(url) + .build() + .into_alien_error() + .context(ErrorData::Other { message: "could not build Foundry GET deployment".to_string() })?; + match self.send(req).await { + Ok(value) => { + let dep: Deployment = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Foundry GetDeployment returned an unexpected body".to_string(), + })?; + Ok(match dep.provisioning_state.as_deref() { + Some("Succeeded") => Some(cap.served_model_id.clone()), + _ => None, + }) + } + Err(e) if is_not_found(&e) => Ok(None), + Err(e) => Err(e), + } + } +} + +fn is_not_found(err: &alien_error::AlienError) -> bool { + matches!( + &err.error, + Some(ErrorData::UpstreamFailed { message }) if message.contains("404") + ) +} diff --git a/crates/alien-gateway/src/finetune/vertex.rs b/crates/alien-gateway/src/finetune/vertex.rs new file mode 100644 index 000000000..c069e7252 --- /dev/null +++ b/crates/alien-gateway/src/finetune/vertex.rs @@ -0,0 +1,243 @@ +//! GCP Vertex AI fine-tuning provider. +//! +//! Submits `POST tuningJobs`, polls `GET tuningJobs/{id}`, and rediscovers the tuned +//! model by listing tuning jobs filtered on the deterministic display name (the +//! stateless gateway keeps no job id). All calls carry the workload's ambient bearer +//! token; the SigV4 service name is unused for bearer auth. +//! +//! Vertex supports only supervised tuning for Gemini (no user-selectable LoRA/DPO), +//! so `method` is not forwarded; the job is always supervised. + +use alien_core::bindings::FinetuneCapability; +use alien_error::{Context, IntoAlienError}; +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::json; + +use crate::creds::AmbientCred; +use crate::error::{ErrorData, Result}; + +use super::{FineTuneProvider, JobHandle, JobState, JobStatus, SubmitRequest}; + +/// Bearer auth ignores the SigV4 service name. +const UNUSED_SERVICE: &str = "aiplatform"; + +pub struct VertexFineTune<'a> { + location: String, + project: String, + cred: &'a AmbientCred, + client: &'a reqwest::Client, + base_override: Option, +} + +impl<'a> VertexFineTune<'a> { + pub fn new( + location: String, + project: String, + cred: &'a AmbientCred, + client: &'a reqwest::Client, + base_override: Option, + ) -> Self { + Self { location, project, cred, client, base_override } + } + + /// Regional Vertex host (`global` uses the un-prefixed host), or the test override. + fn host(&self) -> String { + if let Some(base) = &self.base_override { + return base.clone(); + } + if self.location == "global" { + "https://aiplatform.googleapis.com".to_string() + } else { + format!("https://{}-aiplatform.googleapis.com", self.location) + } + } + + fn jobs_path(&self) -> String { + format!( + "/v1/projects/{}/locations/{}/tuningJobs", + self.project, self.location + ) + } + + async fn send(&self, mut req: reqwest::Request) -> Result { + let url = req.url().to_string(); + self.cred.authorize(&mut req, UNUSED_SERVICE).await?; + let resp = self + .client + .execute(req) + .await + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: format!("Vertex tuning request to {url} failed"), + })?; + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + if !status.is_success() { + return Err(alien_error::AlienError::new(ErrorData::UpstreamFailed { + message: format!("Vertex {status} for {url}: {body}"), + })); + } + if body.is_empty() { + return Ok(serde_json::Value::Null); + } + serde_json::from_str(&body) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: format!("Vertex returned non-JSON from {url}: {body}"), + }) + } +} + +/// A tuning job resource — `name` is the job id, `state` the lifecycle, `tunedModel` +/// the result once succeeded. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TuningJob { + #[serde(default)] + name: Option, + #[serde(default)] + state: Option, + #[serde(default)] + tuned_model: Option, + #[serde(default)] + error: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TunedModelRef { + /// The deployed tuned-model endpoint the OpenAI-compat path targets. + #[serde(default)] + endpoint: Option, + /// The tuned model resource name (fallback if no endpoint). + #[serde(default)] + model: Option, +} + +impl TunedModelRef { + fn upstream_id(&self) -> Option { + self.endpoint.clone().or_else(|| self.model.clone()) + } +} + +#[derive(Deserialize)] +struct StatusError { + #[serde(default)] + message: Option, +} + +/// List response for rediscovery. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ListTuningJobs { + #[serde(default)] + tuning_jobs: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TuningJobWithDisplay { + #[serde(default)] + tuned_model_display_name: Option, + #[serde(default)] + state: Option, + #[serde(default)] + tuned_model: Option, +} + +fn map_state(state: Option<&str>, tuned: Option<&TunedModelRef>, err: Option) -> JobState { + match state { + Some("JOB_STATE_SUCCEEDED") => JobState { + status: JobStatus::Succeeded, + model: tuned.and_then(TunedModelRef::upstream_id), + message: None, + }, + Some("JOB_STATE_FAILED") + | Some("JOB_STATE_CANCELLED") + | Some("JOB_STATE_EXPIRED") + | Some("JOB_STATE_PARTIALLY_SUCCEEDED") => { + JobState { status: JobStatus::Failed, model: None, message: err } + } + _ => JobState { status: JobStatus::Running, model: None, message: None }, + } +} + +#[async_trait] +impl FineTuneProvider for VertexFineTune<'_> { + async fn submit(&self, request: &SubmitRequest<'_>) -> Result { + let cap = request.capability; + let training_uri = format!("gs://{}/{}", cap.training_bucket, request.training_key()); + let body = json!({ + "baseModel": cap.base_model, + "supervisedTuningSpec": { "trainingDatasetUri": training_uri }, + "tunedModelDisplayName": cap.job_name, + }); + let req = self + .client + .post(format!("{}{}", self.host(), self.jobs_path())) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .json(&body) + .build() + .into_alien_error() + .context(ErrorData::Other { message: "could not build Vertex tuningJobs POST".to_string() })?; + let value = self.send(req).await?; + let parsed: TuningJob = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Vertex CreateTuningJob returned no job name".to_string(), + })?; + let job_id = parsed.name.ok_or_else(|| { + alien_error::AlienError::new(ErrorData::UpstreamFailed { + message: "Vertex tuning job response had no name".to_string(), + }) + })?; + Ok(JobHandle { job_id, served_model: cap.served_model_id.clone() }) + } + + async fn status(&self, job_id: &str) -> Result { + // job_id is the full resource name; GET the host + "/v1/{name}". + let req = self + .client + .get(format!("{}/v1/{}", self.host(), job_id)) + .build() + .into_alien_error() + .context(ErrorData::Other { message: "could not build Vertex GET tuningJob".to_string() })?; + let value = self.send(req).await?; + let parsed: TuningJob = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Vertex GetTuningJob returned an unexpected body".to_string(), + })?; + Ok(map_state( + parsed.state.as_deref(), + parsed.tuned_model.as_ref(), + parsed.error.and_then(|e| e.message), + )) + } + + async fn resolve_served_model(&self, cap: &FinetuneCapability) -> Result> { + // No stored job id: list tuning jobs and find the succeeded one whose display + // name matches the deterministic job_name, then take its tuned-model endpoint. + let req = self + .client + .get(format!("{}{}", self.host(), self.jobs_path())) + .build() + .into_alien_error() + .context(ErrorData::Other { message: "could not build Vertex ListTuningJobs".to_string() })?; + let value = self.send(req).await?; + let list: ListTuningJobs = serde_json::from_value(value) + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: "Vertex ListTuningJobs returned an unexpected body".to_string(), + })?; + Ok(list + .tuning_jobs + .into_iter() + .find(|j| { + j.tuned_model_display_name.as_deref() == Some(&cap.job_name) + && j.state.as_deref() == Some("JOB_STATE_SUCCEEDED") + }) + .and_then(|j| j.tuned_model.and_then(|t| t.upstream_id()))) + } +} diff --git a/crates/alien-gateway/src/lib.rs b/crates/alien-gateway/src/lib.rs new file mode 100644 index 000000000..edb416d7b --- /dev/null +++ b/crates/alien-gateway/src/lib.rs @@ -0,0 +1,197 @@ +//! Embedded, protocol-agnostic AI gateway: a loopback HTTP server that injects the +//! workload's ambient cloud credential and proxies each request to the model's +//! native upstream endpoint without translating the body. +//! +//! The runtime starts the gateway before the app process (`start_gateway`) and +//! passes its URL to the app; routing lives in `router`, credential injection in +//! `creds`. + +mod config; +mod creds; +mod error; +mod finetune; +mod router; +pub use config::{bindings_from_env, bindings_from_env_map}; +pub use creds::{AmbientCred, AwsSigV4Cred, BearerTokenCred}; +pub use error::{ErrorData, Result}; +pub use router::{build_router, GatewayRoute}; + +use std::net::{Ipv4Addr, SocketAddr}; + +use alien_core::Platform; +use alien_error::{Context, IntoAlienError}; +use axum::routing::get; + +/// The readiness endpoint the gateway serves and every caller polls. Shared so the +/// path and the poll budget below cannot drift between the runtime and the launcher. +pub const READY_PATH: &str = "/healthz/ready"; +/// Readiness poll budget: `READY_POLL_TRIES` attempts, `READY_POLL_INTERVAL_MS` apart. +/// ~1s covers process spawn + tokio init + local config assembly; ambient credential +/// resolution is lazy (deferred to the first request), so no network I/O gates the bind. +pub const READY_POLL_TRIES: usize = 20; +pub const READY_POLL_INTERVAL_MS: u64 = 50; + +/// One `ai` resource the gateway serves. An Alien `ai` binding maps to exactly one +/// cloud, so each binding fixes the upstream host and ambient credential; the +/// request's model selects the model and (via the catalog) the protocol path. +#[derive(Debug, Clone)] +pub struct GatewayBinding { + /// The binding name (the path segment the app calls: `//...`). + pub name: String, + pub cloud: Platform, + /// AWS region or GCP location. + pub region: Option, + /// GCP project id. + pub project: Option, + /// Azure account endpoint, e.g. `https://acct.openai.azure.com/`. + pub azure_endpoint: Option, + /// A tuned model this binding serves alongside the static catalog, produced + /// by a completed fine-tuning job. `None` for a pure inference gateway. + pub tuned: Option, + /// The fine-tuning capability the runtime control-plane routes use. `None` + /// unless the binding declared `.finetune(...)`. + pub finetune: Option, +} + +/// A tuned model the gateway routes to: the public id an app requests mapped to +/// the provider-native upstream artifact (a Bedrock custom-model ARN, a Vertex +/// tuned endpoint id, or a Foundry deployment name). Populated from the +/// binding's `tunedModel`; consumed by the router before the static catalog. +#[derive(Debug, Clone)] +pub struct TunedRoute { + /// The public model id apps send in the `model` field. + pub served_id: String, + /// The provider-native upstream artifact the gateway forwards to. + pub upstream_id: String, +} + +/// A running gateway: its loopback base URL and the server task that keeps it alive +/// for the process lifetime. Dropping the handle aborts the server. +pub struct GatewayHandle { + pub url: String, + server: tokio::task::JoinHandle<()>, +} + +impl Drop for GatewayHandle { + /// Dropping a `JoinHandle` only detaches its task, so abort explicitly — otherwise the + /// server outlives the handle and keeps the port bound until the process exits. + fn drop(&mut self) { + self.server.abort(); + } +} + +/// Start the gateway on an ephemeral loopback port and return its base URL once the +/// listener is bound. Each binding's ambient credential is resolved up front and +/// mounted as a proxy route under `/`; `/healthz/ready` answers immediately. +pub async fn start_gateway(bindings: Vec) -> Result { + start_gateway_on(bindings, SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).await +} + +/// Like [`start_gateway`] but binds an explicit address. The container launcher uses a +/// fixed loopback port here because it tells the app the URL out of band (an env var) +/// rather than reading back a randomly assigned port. +pub async fn start_gateway_on( + bindings: Vec, + addr: SocketAddr, +) -> Result { + // Built once and shared across routes: on the runtime-less mint path it is the single + // refresh clock and mint cache every request re-resolves through. + let managed = config::managed_provider()?; + let mut routes = Vec::with_capacity(bindings.len()); + for binding in bindings { + routes.push(config::resolve_route(binding, managed.as_ref()).await?); + } + let router = build_router(routes).route(READY_PATH, get(ready)); + + let listener = tokio::net::TcpListener::bind(addr) + .await + .into_alien_error() + .context(ErrorData::BindFailed { + address: addr.to_string(), + })?; + let bound = listener + .local_addr() + .into_alien_error() + .context(ErrorData::Other { + message: "could not read the gateway's bound address".to_string(), + })?; + let url = format!("http://{bound}"); + + let server = tokio::spawn(async move { + if let Err(e) = axum::serve(listener, router).await { + tracing::error!(error = %e, "AI gateway server stopped"); + } + }); + + Ok(GatewayHandle { url, server }) +} + +async fn ready() -> &'static str { + "ready" +} + +/// Blocks until the gateway at `base_url` answers `READY_PATH`, within the shared poll +/// budget. Returns `false` if it never became ready. The launcher (a non-async process +/// entrypoint) uses this; the runtime uses an async twin sharing the same constants. +pub fn wait_until_ready_blocking(base_url: &str) -> bool { + let url = format!("{base_url}{READY_PATH}"); + let client = reqwest::blocking::Client::new(); + for _ in 0..READY_POLL_TRIES { + if let Ok(resp) = client.get(&url).send() { + if resp.status().is_success() { + return true; + } + } + std::thread::sleep(std::time::Duration::from_millis(READY_POLL_INTERVAL_MS)); + } + false +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + + #[tokio::test] + async fn gateway_starts_and_serves_health() { + let handle = start_gateway(vec![]).await.expect("gateway should start"); + let body = reqwest::get(format!("{}/healthz/ready", handle.url)) + .await + .expect("health request should succeed") + .text() + .await + .expect("health body should read"); + assert_eq!(body.trim(), "ready"); + } + + #[tokio::test] + async fn gateway_binds_the_requested_port() { + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 0)); + let handle = start_gateway_on(vec![], addr) + .await + .expect("gateway should start on the given addr"); + // URL reflects a concrete loopback bind, not the placeholder :0 + assert!(handle.url.starts_with("http://127.0.0.1:")); + assert!(!handle.url.ends_with(":0")); + } + + /// Dropping the handle must stop the server: a detached task would keep serving (and + /// keep the port bound) for the rest of the process. + #[tokio::test] + async fn dropping_the_handle_stops_the_server() { + let handle = start_gateway(vec![]).await.expect("gateway should start"); + let url = format!("{}/healthz/ready", handle.url); + assert!(reqwest::get(&url).await.is_ok(), "the gateway serves while the handle is held"); + + drop(handle); + // The abort lands on the next scheduler pass. + tokio::task::yield_now().await; + tokio::time::sleep(Duration::from_millis(50)).await; + + assert!( + reqwest::get(&url).await.is_err(), + "the gateway must stop serving once its handle is dropped" + ); + } +} diff --git a/crates/alien-gateway/src/router.rs b/crates/alien-gateway/src/router.rs new file mode 100644 index 000000000..3d26b8d53 --- /dev/null +++ b/crates/alien-gateway/src/router.rs @@ -0,0 +1,2332 @@ +//! The pure proxy: route a request to the model's native cloud endpoint, inject the +//! workload's ambient credential, and stream the response back without translating +//! the body. The only edit to the request body is rewriting the public model id to +//! the catalog's upstream id; the response (JSON or SSE) is passed through byte-for-byte. + +use std::collections::HashMap; +use std::sync::Arc; + +use alien_core::ai_catalog::{self, Protocol}; +use alien_core::Platform; +use alien_error::{AlienError, Context, IntoAlienError}; +use aws_smithy_eventstream::frame::{DecodedFrame, MessageFrameDecoder}; +use aws_smithy_types::event_stream::Message; +use axum::{ + body::{Body, Bytes}, + extract::{Path, State}, + http::{header, HeaderMap, StatusCode}, + response::{IntoResponse, Json, Response}, + routing::{get, post}, + Router, +}; +use base64::{engine::general_purpose::STANDARD, Engine}; +use futures::StreamExt; +use serde_json::{json, Map, Value}; +use tracing::warn; + +use crate::creds::AmbientCred; +use crate::error::{ErrorData, Result}; + +/// One binding resolved into everything the proxy needs to serve it: the cloud (for +/// catalog filtering and upstream selection), the location fields used to build the +/// upstream URL, and the ambient credential. +pub struct GatewayRoute { + /// The binding name — the first path segment the app calls (`//...`). + pub name: String, + pub cloud: Platform, + /// AWS region or GCP location. + pub region: Option, + /// GCP project id. + pub project: Option, + /// Azure account endpoint, e.g. `https://acct.openai.azure.com/`. + pub azure_endpoint: Option, + pub cred: AmbientCred, + /// When set, upstream requests target this base URL instead of the cloud-derived + /// host (the per-protocol path is still appended). Lets tests aim a binding at a + /// mock upstream. + pub upstream_base_override: Option, + /// A tuned model this route serves alongside the static catalog. Checked + /// before the catalog so a completed fine-tuning job's artifact is reachable + /// under its public `served_id`. `None` for a pure inference gateway. + pub tuned: Option, + /// The fine-tuning capability the runtime control-plane routes use to submit + /// and rediscover jobs. `None` if the binding declared no `.finetune(...)`. + pub finetune: Option, +} + +struct AppState { + routes: HashMap, + client: reqwest::Client, +} + +/// Build the axum router serving every binding under `//...`: +/// `POST //v1/chat/completions` (OpenAI), `POST //v1/messages` +/// (Anthropic), and `GET //v1/models`. +pub fn build_router(routes: Vec) -> Router { + let routes = routes.into_iter().map(|r| (r.name.clone(), r)).collect(); + let state = Arc::new(AppState { + routes, + client: reqwest::Client::new(), + }); + Router::new() + .route("/{binding}/v1/chat/completions", post(proxy)) + .route("/{binding}/v1/messages", post(proxy)) + .route("/{binding}/v1/responses", post(proxy_responses)) + .route("/{binding}/v1/models", get(list_models)) + // Runtime fine-tuning control plane (see `crate::finetune`). + .route("/{binding}/v1/finetune", post(submit_finetune)) + .route("/{binding}/v1/finetune/{job}", get(finetune_status)) + .with_state(state) +} + +/// `POST //v1/finetune` — submit a tuning job for the binding's declared +/// capability. Body: optional `{ "trainingKey": "..." }` to override the training +/// file. Returns `{ "jobId", "servedModel" }`. +async fn submit_finetune( + State(state): State>, + Path(binding): Path, + body: Bytes, +) -> Result { + let route = state.routes.get(&binding).ok_or_else(|| { + AlienError::new(ErrorData::UnknownBinding { binding: binding.clone() }) + })?; + let capability = route + .finetune + .as_ref() + .ok_or_else(|| crate::finetune::no_capability(&binding))?; + let provider = crate::finetune::provider_for(&crate::finetune::ProviderCtx { + cloud: route.cloud, + region: route.region.as_deref(), + project: route.project.as_deref(), + azure_endpoint: route.azure_endpoint.as_deref(), + cred: &route.cred, + client: &state.client, + base_override: route.upstream_base_override.as_deref(), + }) + .ok_or_else(|| { + AlienError::new(ErrorData::InvalidRequest { + message: format!("the gateway does not fine-tune {:?} bindings yet", route.cloud), + }) + })?; + + // An empty body is fine (use the declared training key); otherwise parse the override. + let training_key = if body.is_empty() { + None + } else { + let parsed: SubmitFinetuneBody = serde_json::from_slice(&body).map_err(|e| { + AlienError::new(ErrorData::InvalidRequest { + message: format!("invalid finetune body: {e}"), + }) + })?; + parsed.training_key + }; + + let handle = provider + .submit(&crate::finetune::SubmitRequest { capability, training_key }) + .await?; + Ok(Json(handle).into_response()) +} + +/// `GET //v1/finetune/` — poll a submitted job. Returns +/// `{ "status", "model"?, "message"? }`. +async fn finetune_status( + State(state): State>, + Path((binding, job)): Path<(String, String)>, +) -> Result { + let route = state.routes.get(&binding).ok_or_else(|| { + AlienError::new(ErrorData::UnknownBinding { binding: binding.clone() }) + })?; + let provider = crate::finetune::provider_for(&crate::finetune::ProviderCtx { + cloud: route.cloud, + region: route.region.as_deref(), + project: route.project.as_deref(), + azure_endpoint: route.azure_endpoint.as_deref(), + cred: &route.cred, + client: &state.client, + base_override: route.upstream_base_override.as_deref(), + }) + .ok_or_else(|| { + AlienError::new(ErrorData::InvalidRequest { + message: format!("the gateway does not fine-tune {:?} bindings yet", route.cloud), + }) + })?; + + let state_out = provider.status(&job).await?; + Ok(Json(state_out).into_response()) +} + +/// Optional body for `POST /finetune`. +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SubmitFinetuneBody { + #[serde(default)] + training_key: Option, +} + +/// Parse a proxied request body as JSON and pull out its required `model` field. +/// Both the chat/completions|messages handler and the Responses handler route on +/// the request's `model`, so they share this preamble. +fn parse_model_request(body: &[u8]) -> Result<(Value, String)> { + let payload: Value = serde_json::from_slice(body) + .into_alien_error() + .context(ErrorData::InvalidRequest { + message: "request body is not valid JSON".to_string(), + })?; + let model = payload + .get("model") + .and_then(Value::as_str) + .ok_or_else(|| { + AlienError::new(ErrorData::InvalidRequest { + message: "request body has no \"model\" field".to_string(), + }) + })? + .to_string(); + Ok((payload, model)) +} + +/// Forward an upstream reply to the client untouched: its status, content-type, and +/// body, streamed straight through. Streaming the body works identically for a +/// single JSON object and for an SSE stream. +fn forward_response(upstream: reqwest::Response) -> Result { + let status = + StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); + let content_type = upstream.headers().get(header::CONTENT_TYPE).cloned(); + let mut response = Response::builder().status(status); + if let Some(ct) = content_type { + response = response.header(header::CONTENT_TYPE, ct); + } + response + .body(Body::from_stream(upstream.bytes_stream())) + .into_alien_error() + .context(ErrorData::Other { + message: "could not build the proxied response".to_string(), + }) +} + +/// Build a JSON POST to `url`, sign it with the ambient credential for `service`, +/// and execute it. The handlers differ only in URL, signing service, body, and any +/// protocol-required header, so the build + sign + execute + upstream-error +/// scaffolding lives here once. +async fn sign_and_execute( + client: &reqwest::Client, + cred: &AmbientCred, + url: &str, + service: &str, + body: Vec, + extra_headers: &[(&str, &str)], +) -> Result { + let mut builder = client + .post(url) + .header(header::CONTENT_TYPE, "application/json"); + for (name, value) in extra_headers { + builder = builder.header(*name, *value); + } + let mut req = builder + .body(body) + .build() + .into_alien_error() + .context(ErrorData::Other { + // The url names which upstream failed; the handlers otherwise share + // this message and a bare one cannot be traced back to a path. + message: format!("could not build the upstream request to {url}"), + })?; + cred.authorize(&mut req, service).await?; + client + .execute(req) + .await + .into_alien_error() + .context(ErrorData::UpstreamFailed { + message: format!("request to {url} failed"), + }) +} + +/// Proxy a chat/completions or messages request. Routes purely by the request's +/// `model` (the catalog is the single source of truth for protocol + cloud), so the +/// same handler serves both the OpenAI and Anthropic entry paths. +async fn proxy( + State(state): State>, + Path(binding): Path, + headers: HeaderMap, + body: Bytes, +) -> Result { + let route = state.routes.get(&binding).ok_or_else(|| { + AlienError::new(ErrorData::UnknownBinding { + binding: binding.clone(), + }) + })?; + + let (payload, model) = parse_model_request(&body)?; + + // A tuned model produced by a fine-tuning job is served under its public + // `served_id` and is not in the static catalog (its upstream artifact is + // created at runtime). Check it before the catalog. All three clouds serve a + // tuned model over the OpenAI-compatible chat path: on Bedrock the + // `upstream_id` is a custom-model id, on Vertex a tuned-endpoint model id, on + // Foundry a deployment name — in every case it becomes the request body's + // `model` against the cloud's OpenAI endpoint. + // + // Two sources, in order: + // 1. A binding that already carries a resolved `tuned` model (deploy-time + // path / test injection) routes straight to it. + // 2. Otherwise, if the request's model matches the fine-tuning capability's + // `served_model_id`, rediscover the completed tuned model by convention + // (stateless — no stored ARN). If it isn't ready yet, fall through to the + // catalog, which yields `ModelNotAvailable` for the tuned id. + if let Some(tuned) = &route.tuned { + if model == tuned.served_id { + return proxy_openai(&state, route, tuned.upstream_id.clone(), payload).await; + } + } + if let Some(cap) = &route.finetune { + if model == cap.served_model_id { + if let Some(provider) = crate::finetune::provider_for(&crate::finetune::ProviderCtx { + cloud: route.cloud, + region: route.region.as_deref(), + project: route.project.as_deref(), + azure_endpoint: route.azure_endpoint.as_deref(), + cred: &route.cred, + client: &state.client, + base_override: route.upstream_base_override.as_deref(), + }) { + if let Some(upstream) = provider.resolve_served_model(cap).await? { + return proxy_openai(&state, route, upstream, payload).await; + } + } + } + } + + // Cloud-scoped resolution: Claude ids appear once per cloud, so a first-match + // resolve would always land on another cloud's entry and fail the cloud filter. + let cm = ai_catalog::resolve_for(&model, route.cloud).ok_or_else(|| { + AlienError::new(ErrorData::ModelNotAvailable { + model: model.clone(), + binding: binding.clone(), + }) + })?; + + // AWS serves Claude through classic Bedrock InvokeModel, not the passthrough + // endpoint: the model id travels in the URL and the streamed reply is AWS + // event-stream framing, so it needs its own request/response shape. + if route.cloud == Platform::Aws && cm.protocol == Protocol::Anthropic { + return proxy_bedrock_anthropic(&state.client, route, cm.upstream_id, payload, &headers) + .await; + } + // GCP serves Claude through Vertex rawPredict: the model id travels in the URL + // and streaming is chosen by the URL verb, but the reply is native Anthropic + // JSON/SSE — no decoder needed, unlike Bedrock. + if route.cloud == Platform::Gcp && cm.protocol == Protocol::Anthropic { + return proxy_vertex_anthropic(&state.client, route, cm.upstream_id, payload, &headers) + .await; + } + // Azure serves Claude through Foundry's Anthropic endpoint: standard Messages + // in both directions, on the `/anthropic/v1` path with the version header. + if route.cloud == Platform::Azure && cm.protocol == Protocol::Anthropic { + return proxy_foundry_anthropic(&state.client, route, cm.upstream_id, payload, &headers) + .await; + } + + proxy_openai(&state, route, cm.upstream_id.to_string(), payload).await +} + +/// Forward an OpenAI-protocol chat request: rewrite the body `model` to +/// `upstream_id`, build the cloud's OpenAI chat endpoint, sign, and stream the +/// reply back untranslated. Shared by base-catalog models and tuned models. +async fn proxy_openai( + state: &AppState, + route: &GatewayRoute, + upstream_id: String, + mut payload: Value, +) -> Result { + payload["model"] = Value::String(upstream_id); + let upstream_body = serde_json::to_vec(&payload) + .into_alien_error() + .context(ErrorData::Other { + message: "could not re-serialize the rewritten request body".to_string(), + })?; + + let (url, aws_service) = upstream_target(route, Protocol::OpenAi)?; + + let upstream = + sign_and_execute(&state.client, &route.cred, &url, aws_service, upstream_body, &[]).await?; + + forward_response(upstream) +} + +/// Proxy an OpenAI Responses request (`POST //v1/responses`, used by Codex). +/// AWS serves the Responses API natively on the bedrock-mantle endpoint, so this is +/// the same pure passthrough as `proxy` — rewrite the model id, sign, stream back — +/// but aimed at the mantle endpoint. Only AWS OpenAI-protocol models are servable here: the +/// other clouds don't expose a Responses endpoint, and Claude on mantle is +/// Messages-only. +async fn proxy_responses( + State(state): State>, + Path(binding): Path, + body: Bytes, +) -> Result { + let route = state.routes.get(&binding).ok_or_else(|| { + AlienError::new(ErrorData::UnknownBinding { + binding: binding.clone(), + }) + })?; + + let (mut payload, model) = parse_model_request(&body)?; + + // The Responses table implies AWS; the binding's cloud must still match so a + // GCP/Azure binding doesn't forward to an AWS endpoint it has no credential for. + let upstream_id = ai_catalog::responses_upstream_id(&model) + .filter(|_| route.cloud == Platform::Aws) + .ok_or_else(|| { + AlienError::new(ErrorData::ModelNotAvailable { + model: model.clone(), + binding: binding.clone(), + }) + })?; + + payload["model"] = Value::String(upstream_id.to_string()); + let upstream_body = serde_json::to_vec(&payload) + .into_alien_error() + .context(ErrorData::Other { + message: "could not re-serialize the rewritten request body".to_string(), + })?; + + let region = route.region.as_deref().ok_or_else(|| missing_field(route, "region"))?; + let base = route + .upstream_base_override + .clone() + .unwrap_or_else(|| format!("https://bedrock-mantle.{region}.api.aws")); + let url = format!("{}/v1/responses", base.trim_end_matches('/')); + + let upstream = + sign_and_execute(&state.client, &route.cred, &url, "bedrock-mantle", upstream_body, &[]) + .await?; + + forward_response(upstream) +} + +/// `GET //v1/models` — the binding's cloud's curated catalog, in OpenAI list shape. +async fn list_models( + State(state): State>, + Path(binding): Path, +) -> Result { + let route = state.routes.get(&binding).ok_or_else(|| { + AlienError::new(ErrorData::UnknownBinding { binding }) + })?; + let data: Vec = ai_catalog::models_for(route.cloud) + .iter() + .map(|m| json!({ "id": m.public_id, "object": "model" })) + .collect(); + Ok(Json(json!({ "object": "list", "data": data })).into_response()) +} + +/// The error for a binding missing a field a handler needs. +fn missing_field(route: &GatewayRoute, field: &str) -> AlienError { + AlienError::new(ErrorData::BindingConfigInvalid { + binding: route.name.clone(), + message: format!("it is missing its {field}"), + }) +} + +/// The upstream URL and (for AWS) the SigV4 service name for a binding + protocol. +fn upstream_target(route: &GatewayRoute, protocol: Protocol) -> Result<(String, &'static str)> { + let (host, path, aws_service) = match (route.cloud, protocol) { + (Platform::Aws, Protocol::OpenAi) => { + let region = route.region.as_deref().ok_or_else(|| missing_field(route, "region"))?; + ( + format!("https://bedrock-runtime.{region}.amazonaws.com"), + "/openai/v1/chat/completions".to_string(), + "bedrock", + ) + } + (Platform::Gcp, Protocol::OpenAi) => { + let location = + route.region.as_deref().ok_or_else(|| missing_field(route, "location"))?; + let project = + route.project.as_deref().ok_or_else(|| missing_field(route, "project"))?; + ( + vertex_host(location), + format!( + "/v1/projects/{project}/locations/{location}/endpoints/openapi/chat/completions" + ), + "", + ) + } + (Platform::Azure, Protocol::OpenAi) => { + let endpoint = + route.azure_endpoint.as_deref().ok_or_else(|| missing_field(route, "endpoint"))?; + ( + endpoint.trim_end_matches('/').to_string(), + "/openai/v1/chat/completions".to_string(), + "", + ) + } + (cloud, proto) => { + return Err(AlienError::new(ErrorData::Other { + message: format!("{cloud:?} does not serve the {proto:?} protocol"), + })) + } + }; + + let base = route + .upstream_base_override + .clone() + .unwrap_or(host); + Ok((format!("{}{}", base.trim_end_matches('/'), path), aws_service)) +} + +/// Read a request's `stream` field. Streaming picks between two different +/// upstream shapes, so a malformed value must be a loud 400 — coercing it would +/// answer an SSE client with a JSON body it can only interpret as a hang. +fn parse_stream_flag(value: Option) -> Result { + match value { + None | Some(Value::Null) => Ok(false), + Some(Value::Bool(value)) => Ok(value), + Some(_) => Err(AlienError::new(ErrorData::InvalidRequest { + message: "the `stream` field must be a boolean".to_string(), + })), + } +} + +/// The Vertex AI Platform host for a location: the global endpoint is the +/// un-prefixed host; a region prefixes it. The path carries `locations/{location}` +/// either way. +fn vertex_host(location: &str) -> String { + if location == "global" { + "https://aiplatform.googleapis.com".to_string() + } else { + format!("https://{location}-aiplatform.googleapis.com") + } +} + +/// Serve a Claude request through Vertex `rawPredict`. Nearly the Anthropic +/// Messages API: the model id travels in the URL, streaming picks the URL verb +/// (`:streamRawPredict`), and the body carries Vertex's version marker instead of +/// a `model`. The reply is native Anthropic JSON/SSE, so unlike the Bedrock shim +/// there is no event-stream decoder — and betas ride the standard `anthropic-beta` +/// header rather than a body field, since Vertex speaks the native Messages API. +async fn proxy_vertex_anthropic( + client: &reqwest::Client, + route: &GatewayRoute, + upstream_id: &str, + mut payload: Value, + headers: &HeaderMap, +) -> Result { + let location = route.region.as_deref().ok_or_else(|| missing_field(route, "location"))?; + let project = route.project.as_deref().ok_or_else(|| missing_field(route, "project"))?; + + let obj = payload.as_object_mut().ok_or_else(|| { + AlienError::new(ErrorData::InvalidRequest { + message: "request body must be a JSON object".to_string(), + }) + })?; + obj.remove("model"); + obj.insert("anthropic_version".to_string(), json!("vertex-2023-10-16")); + // The `stream` field stays in the body; Vertex accepts it alongside the verb. + let stream = parse_stream_flag(obj.get("stream").cloned())?; + let verb = if stream { "streamRawPredict" } else { "rawPredict" }; + + let base = route.upstream_base_override.clone().unwrap_or_else(|| vertex_host(location)); + let url = format!( + "{}/v1/projects/{project}/locations/{location}/publishers/anthropic/models/{upstream_id}:{verb}", + base.trim_end_matches('/') + ); + + let upstream_body = serde_json::to_vec(&payload) + .into_alien_error() + .context(ErrorData::Other { + message: "could not re-serialize the rewritten request body".to_string(), + })?; + + // Vertex is the native Messages API, so betas ride the standard header — + // filtered through the same allowlist that keeps Anthropic-API-side markers + // (notably oauth-2025-04-20) from turning the request into a 400. + let betas = filtered_header_betas(headers).join(","); + let mut extra_headers: Vec<(&str, &str)> = Vec::new(); + if !betas.is_empty() { + extra_headers.push(("anthropic-beta", betas.as_str())); + } + let upstream = + sign_and_execute(client, &route.cred, &url, "", upstream_body, &extra_headers).await?; + forward_response(upstream) +} + +/// The Messages API version the gateway bridges to Foundry's Anthropic endpoint; +/// Foundry reads it from the standard `anthropic-version` header. +const FOUNDRY_ANTHROPIC_VERSION: &str = "2023-06-01"; + +/// Serve a Claude request through Foundry's Anthropic endpoint. The closest arm to +/// the plain passthrough: the model stays in the body (rewritten to the Foundry +/// deployment name), streaming is the standard body field, and the reply is native +/// Anthropic JSON/SSE — only the version header and the `/anthropic/v1` path +/// distinguish it from the OpenAI arm. +async fn proxy_foundry_anthropic( + client: &reqwest::Client, + route: &GatewayRoute, + upstream_id: &str, + mut payload: Value, + headers: &HeaderMap, +) -> Result { + let endpoint = + route.azure_endpoint.as_deref().ok_or_else(|| missing_field(route, "endpoint"))?; + + payload["model"] = Value::String(upstream_id.to_string()); + let upstream_body = serde_json::to_vec(&payload) + .into_alien_error() + .context(ErrorData::Other { + message: "could not re-serialize the rewritten request body".to_string(), + })?; + + // The binding carries the AIServices account endpoint; the Anthropic path + // serves on that account. Whether the account host also needs the Entra + // audience swapped to https://ai.azure.com is settled by the live Foundry + // probe — the credential keeps the account audience until that probe says + // otherwise. + let base = route + .upstream_base_override + .clone() + .unwrap_or_else(|| endpoint.to_string()); + let url = format!("{}/anthropic/v1/messages", base.trim_end_matches('/')); + + // Foundry speaks the standard Anthropic protocol, so betas ride the standard + // header — filtered through the same allowlist that keeps Anthropic-API-side + // markers from turning the request into a 400. + let betas = filtered_header_betas(headers).join(","); + let mut extra_headers = vec![("anthropic-version", FOUNDRY_ANTHROPIC_VERSION)]; + if !betas.is_empty() { + extra_headers.push(("anthropic-beta", betas.as_str())); + } + let upstream = + sign_and_execute(client, &route.cred, &url, "", upstream_body, &extra_headers).await?; + forward_response(upstream) +} + +/// The client-executed tool families Bedrock hosts on classic `InvokeModel` +/// (verified against AWS docs). Anything else typed is server-executed by Anthropic's +/// own API servers, which Bedrock is not, so it is dropped rather than 400'd. +const BEDROCK_HOSTED_TOOL_PREFIXES: &[&str] = + &["bash_", "text_editor_", "computer_", "memory_", "tool_search_"]; + +/// The `anthropic_beta` families Bedrock's classic `InvokeModel` accepts in the body +/// (each live-verified: an accepted tag returns 200, an unknown one is a +/// ValidationException "invalid beta flag"). Anthropic-API-side markers — notably +/// `oauth-2025-04-20`, which every OAuth-authenticated Claude Code request declares — +/// are rejected, so the header bridge folds only these families across. +const BEDROCK_BETA_PREFIXES: &[&str] = &[ + "claude-code-", + "computer-use-", + "context-1m-", + "context-management-", + "fine-grained-tool-streaming-", + "interleaved-thinking-", + "output-128k-", + "token-efficient-tools-", + "tool-examples-", +]; + +/// Serve a Claude request through classic Bedrock `InvokeModel`. The Anthropic +/// Messages body *is* the InvokeModel body, but the model id travels in the URL +/// (as a cross-region inference profile) and streaming is chosen by the URL +/// suffix — so, unlike the passthrough path, the body carries neither, and the +/// streamed reply arrives as AWS event-stream framing we decode back into the +/// Anthropic SSE the client expects. +/// +/// This whole function is a protocol shim, kept only until Bedrock's mantle +/// endpoint serves Claude on the same standard model access InvokeModel already +/// grants. Where mantle does not yet serve Claude for a given account/region it +/// returns 403 while the same model and credential serve 200 via InvokeModel; +/// this shim bridges that gap. Drop it (and the decoder below) in favor of the +/// plain mantle passthrough once mantle serves Claude directly — to check whether +/// a region already does: +/// +/// ```text +/// curl --aws-sigv4 "aws:amz::bedrock-mantle" --user "$KEY:$SECRET" \ +/// -H "x-amz-security-token: $TOKEN" -H "content-type: application/json" \ +/// -d '{"model":"anthropic.claude-haiku-4-5","max_tokens":16, +/// "messages":[{"role":"user","content":"Say ok"}]}' \ +/// https://bedrock-mantle..api.aws/anthropic/v1/messages +/// ``` +/// +/// Bedrock's Converse API was evaluated (live, 2026-07-16) and rejected: it +/// works on standard access, but it speaks AWS's own schema in both directions, +/// so it would replace these targeted fixups with a full Anthropic⇄Converse +/// codec (content taxonomy, toolSpec, synthesized message ids, a Converse-event +/// stream translation) while still needing the event-stream decoder, the system +/// fold, the server-tool filter, and the beta bridge in relocated form. +async fn proxy_bedrock_anthropic( + client: &reqwest::Client, + route: &GatewayRoute, + upstream_id: &str, + mut payload: Value, + headers: &HeaderMap, +) -> Result { + let region = route.region.as_deref().ok_or_else(|| missing_field(route, "region"))?; + + let obj = payload.as_object_mut().ok_or_else(|| { + AlienError::new(ErrorData::InvalidRequest { + message: "request body must be a JSON object".to_string(), + }) + })?; + // The model is in the URL and streaming is chosen by the URL suffix, so neither + // belongs in the body; Bedrock requires its own version marker there instead. + obj.remove("model"); + // Bedrock's schema rejects a body `stream` field, so it is removed here. + let stream = parse_stream_flag(obj.remove("stream"))?; + obj.insert("anthropic_version".to_string(), json!("bedrock-2023-05-31")); + + // Claude clients declare betas in the `anthropic-beta` HTTP header, but classic + // InvokeModel reads only the body's `anthropic_beta`. Bridge the Bedrock-known + // families across so a beta-gated tool we forward (computer_*, memory_*) arrives + // with the beta it needs — and drop the rest, which Bedrock's body validation + // rejects (see merge_beta_headers). + merge_beta_headers(obj, headers); + + // Bedrock's InvokeModel schema (pinned to `bedrock-2023-05-31`) predates the + // newest Anthropic Messages fields, so a latest client (Claude Code) sends fields + // it rejects. Drop the ones outside its schema so the request isn't a 400 — the + // gateway is bridging a protocol-version gap, not the raw native endpoint. + obj.remove("output_config"); + obj.remove("context_management"); + // Bedrock supports only `enabled`/`disabled` extended thinking; drop a newer mode + // (e.g. `adaptive`) rather than let Bedrock reject the whole request. + let thinking_unsupported = obj + .get("thinking") + .and_then(|t| t.get("type")) + .and_then(Value::as_str) + .is_some_and(|t| t != "enabled" && t != "disabled"); + if thinking_unsupported { + obj.remove("thinking"); + } + // Anthropic *server*-executed tool types (web_search, code_execution, web fetch, + // advisor) run on Anthropic's own API servers, which InvokeModel is not, so + // Bedrock rejects them; drop those and keep the families Bedrock hosts. Dropping + // (rather than a 400) is deliberate bridge behavior — Claude Code declares web + // tools by default, so rejecting would fail its every request — but it must stay + // visible in the logs and leave the body coherent: Bedrock rejects a `tool_choice` + // that forces a tool that is no longer declared, and an emptied `tools` array. + // A kept beta-gated family still needs its `anthropic_beta` entry, which + // merge_beta_headers has already bridged from the header above. Also strip + // `defer_loading`, a client-tool field Claude Code's on-demand tool loading adds + // that the pinned schema rejects as an extra input. + // + // Residue blocks a *previous* server-tool turn left in `messages` (e.g. + // `web_search_tool_result` from a conversation started on Anthropic's API) are + // deliberately NOT rewritten: Bedrock knows those block types and rejects foreign + // ones loudly (live-verified: Anthropic-issued `encrypted_content` fails its + // validation), and that 400 reaches the client via forward_response — a loud, + // honest failure, where stripping would silently alter the conversation. + let mut dropped_tools: Vec = Vec::new(); + let mut tools_remaining = true; + if let Some(tools) = obj.get_mut("tools").and_then(Value::as_array_mut) { + tools.retain(|tool| { + let keep = match tool.get("type").and_then(Value::as_str) { + // Plain client tools carry no type, or `custom`. + None | Some("custom") => true, + Some(tag) => BEDROCK_HOSTED_TOOL_PREFIXES.iter().any(|p| tag.starts_with(p)), + }; + if !keep { + let label = tool + .get("name") + .or_else(|| tool.get("type")) + .and_then(Value::as_str) + .unwrap_or("unnamed"); + dropped_tools.push(label.to_string()); + } + keep + }); + for tool in tools.iter_mut() { + if let Some(obj) = tool.as_object_mut() { + obj.remove("defer_loading"); + } + } + tools_remaining = !tools.is_empty(); + } + if !dropped_tools.is_empty() { + warn!( + binding = %route.name, + tools = %dropped_tools.join(", "), + "dropped Anthropic server-executed tools that Bedrock InvokeModel cannot serve" + ); + if !tools_remaining { + obj.remove("tools"); + obj.remove("tool_choice"); + } else { + let forces_dropped = obj + .get("tool_choice") + .and_then(|choice| choice.get("name")) + .and_then(Value::as_str) + .is_some_and(|name| dropped_tools.iter().any(|dropped| dropped == name)); + if forces_dropped { + obj.remove("tool_choice"); + } + } + } + // The pinned schema also predates mid-conversation `system` roles inside + // `messages` (top-level `system` is its only sanctioned spot) and enforces + // user/assistant alternation. Re-tag those turns as `user` where they stand — + // their position in the conversation is what carries the meaning — then fold + // same-role neighbors into one message so alternation still holds. + if let Some(messages) = obj.get_mut("messages").and_then(Value::as_array_mut) { + let originals = std::mem::take(messages); + for mut message in originals { + if message.get("role").and_then(Value::as_str) == Some("system") { + message["role"] = json!("user"); + } + let same_role_as_last = messages + .last() + .and_then(|previous| previous.get("role")) + .is_some_and(|role| Some(role) == message.get("role")); + if let Some(previous) = messages.last_mut().filter(|_| same_role_as_last) { + let addition = take_content_blocks(&mut message)?; + let merged = ensure_block_content(previous)?; + // A tool_use turn must be answered by tool_result blocks at the START + // of the next message (live-verified: Bedrock 400s on `[text, + // tool_result]`), so when the folded-in neighbor carries results — + // e.g. a downgraded system turn landed between a tool call and its + // result — they slot in right after any results already leading. + let (tool_results, rest): (Vec, Vec) = + addition.into_iter().partition(|block| { + block.get("type").and_then(Value::as_str) == Some("tool_result") + }); + let leading = merged + .iter() + .take_while(|block| { + block.get("type").and_then(Value::as_str) == Some("tool_result") + }) + .count(); + merged.splice(leading..leading, tool_results); + merged.extend(rest); + } else { + messages.push(message); + } + } + } + + let upstream_body = serde_json::to_vec(&payload) + .into_alien_error() + .context(ErrorData::Other { + message: "could not re-serialize the Bedrock request body".to_string(), + })?; + + let suffix = if stream { "invoke-with-response-stream" } else { "invoke" }; + let model_id = format!("{}.{}", bedrock_geo(region), upstream_id); + let base = route + .upstream_base_override + .clone() + .unwrap_or_else(|| format!("https://bedrock-runtime.{region}.amazonaws.com")); + let url = format!("{}/model/{}/{}", base.trim_end_matches('/'), model_id, suffix); + + let upstream = sign_and_execute(client, &route.cred, &url, "bedrock", upstream_body, &[]).await?; + + let status = + StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); + + // Only a 2xx streaming reply is event-stream framed. A non-2xx (throttling, + // missing model access, bad request) and every non-streaming reply are plain + // JSON, so forward them untouched — the client sees the real status and body. + if !upstream.status().is_success() || !stream { + return forward_response(upstream); + } + + // Decode the event-stream frames into Anthropic SSE as they arrive (the decoder + // buffers across network chunks, so partial frames don't corrupt the output), + // then flush once the upstream closes: a stream that ended mid-frame surfaces a + // loud error via finish() instead of a silently truncated reply. + let sse = futures::stream::unfold( + (Box::pin(upstream.bytes_stream()), EventStreamToSse::default(), false), + |(mut body, mut decoder, done)| async move { + if done { + return None; + } + match body.next().await { + Some(Ok(bytes)) => { + Some((Ok(Bytes::from(decoder.push(&bytes))), (body, decoder, false))) + } + Some(Err(err)) => Some((Err(err), (body, decoder, true))), + // Upstream closed: emit the end-of-stream flush, then stop. + None => Some((Ok(Bytes::from(decoder.finish())), (body, decoder, true))), + } + }, + ); + Response::builder() + .status(status) + .header(header::CONTENT_TYPE, "text/event-stream") + .body(Body::from_stream(sse)) + .into_alien_error() + .context(ErrorData::Other { + message: "could not build the streamed response".to_string(), + }) +} + +/// Merge the request's `anthropic-beta` headers into the body's `anthropic_beta`. +/// The header may repeat and each value may be comma-separated; the body field takes +/// an array or a single string. Body entries are kept and duplicates dropped, so a +/// client may declare betas any of those ways. +/// +/// Only BEDROCK_BETA_PREFIXES families are bridged. Bedrock ignores unknown tags in +/// the *header* but validates the *body* list, so folding an Anthropic-API-side +/// marker across turns the whole request into a ValidationException. Body entries +/// stay unfiltered: a client authoring Bedrock-dialect JSON asked for exactly that +/// list and gets Bedrock's loud answer. +fn merge_beta_headers(obj: &mut Map, headers: &HeaderMap) { + let mut betas: Vec = match obj.get("anthropic_beta") { + Some(Value::Array(list)) => { + list.iter().filter_map(|v| v.as_str().map(str::to_owned)).collect() + } + Some(Value::String(tag)) => vec![tag.clone()], + _ => Vec::new(), + }; + for beta in filtered_header_betas(headers) { + if !betas.iter().any(|existing| existing == &beta) { + betas.push(beta); + } + } + if !betas.is_empty() { + obj.insert("anthropic_beta".to_string(), json!(betas)); + } +} + +/// The client's `anthropic-beta` declarations that pass the allowlist. The header +/// may repeat and each value may be comma-separated. Filtering is an allowlist +/// because these endpoints validate what they are handed: an Anthropic-API-side +/// marker (notably `oauth-2025-04-20`, declared by every OAuth Claude Code +/// request) turns the whole request into a 400. Vertex and Foundry reuse the +/// Bedrock-verified families until their own live probes verify per-upstream +/// lists. +fn filtered_header_betas(headers: &HeaderMap) -> Vec { + let mut kept: Vec = Vec::new(); + let mut dropped: Vec = Vec::new(); + for value in headers.get_all("anthropic-beta") { + let Ok(raw) = value.to_str() else { continue }; + for beta in raw.split(',').map(str::trim).filter(|b| !b.is_empty()) { + if !BEDROCK_BETA_PREFIXES.iter().any(|p| beta.starts_with(p)) { + dropped.push(beta.to_string()); + continue; + } + if !kept.iter().any(|existing| existing == beta) { + kept.push(beta.to_string()); + } + } + } + if !dropped.is_empty() { + warn!( + betas = %dropped.join(", "), + "dropped anthropic-beta tags outside the allowlisted families" + ); + } + kept +} + +/// Normalize a message's `content` to a block array and hand the array back, so two +/// messages folding into one can concatenate their block lists. A string becomes a +/// single text block; an existing array is kept; any other shape (missing, null, an +/// object) is a malformed message the native Anthropic endpoint would reject — fail +/// loud rather than fold the turn into an empty array and answer a conversation the +/// client didn't send. +fn ensure_block_content(message: &mut Value) -> Result<&mut Vec> { + match message.get("content") { + Some(Value::Array(_)) => {} + Some(Value::String(text)) => { + message["content"] = json!([{ "type": "text", "text": text }]); + } + _ => { + return Err(AlienError::new(ErrorData::InvalidRequest { + message: "every message `content` must be a string or an array of blocks" + .to_string(), + })) + } + } + Ok(message["content"] + .as_array_mut() + .expect("content was just normalized to an array")) +} + +/// Take a message's content as a block list, leaving the message with an empty one. +fn take_content_blocks(message: &mut Value) -> Result> { + Ok(std::mem::take(ensure_block_content(message)?)) +} + +/// The cross-region inference-profile geo prefix for a Bedrock region. Claude on +/// Bedrock is invocable only through a geo profile (e.g. `us.anthropic.…`). +/// +/// us / us-gov regions keep their own geo. Every other commercial region routes via +/// the region-agnostic `global` profile: current-generation Claude models publish a +/// `global.` inference profile invocable from any commercial region (verified against +/// live Bedrock), and do NOT publish `eu.`/`apac.` profiles, so a per-continent +/// prefix would build a non-existent id. (An older model that publishes only a `us.` +/// profile, e.g. opus-4.1, stays us-region-only either way.) +fn bedrock_geo(region: &str) -> &'static str { + if region.starts_with("us-gov-") { + "us-gov" + } else if region.starts_with("us-") { + "us" + } else { + "global" + } +} + +/// Decoder turning Bedrock's `vnd.amazon.eventstream` framing into Anthropic SSE. +/// A normal chunk frame's payload is `{"bytes": base64()}`; +/// the decoded event carries a `type` we surface as the SSE `event:` name. Network +/// chunks can split or merge frames, so bytes are buffered until each frame is +/// whole. Frame parsing (prelude, headers, both CRC32 checks) is +/// aws-smithy-eventstream's `MessageFrameDecoder` — AWS's own decoder for this +/// wire format — so a corrupted or desynced stream fails its CRCs instead of +/// decoding to garbage. +#[derive(Default)] +struct EventStreamToSse { + buf: Vec, + /// Set once a frame fails to decode (a CRC mismatch or malformed prelude). + /// From then on the buffer can never be drained, so we stop parsing rather than + /// spin on it forever. + failed: bool, +} + +impl EventStreamToSse { + /// Append a network chunk and return the SSE for every frame it now completes. + fn push(&mut self, chunk: &[u8]) -> String { + if self.failed { + return String::new(); + } + self.buf.extend_from_slice(chunk); + let mut out = String::new(); + // A fresh decoder scans the buffer from the top each push, and only the bytes + // of fully decoded frames are drained: MessageFrameDecoder consumes a prelude + // into internal state before its frame completes, so reusing one across + // pushes would strand those bytes between the two buffers. + let mut decoder = MessageFrameDecoder::new(); + let mut cursor: &[u8] = &self.buf; + let mut consumed = 0; + loop { + match decoder.decode_frame(&mut cursor) { + Ok(DecodedFrame::Complete(message)) => { + consumed = self.buf.len() - cursor.len(); + out.push_str(&message_to_sse(&message)); + } + Ok(DecodedFrame::Incomplete) => break, + Err(_) => { + // A CRC mismatch or malformed prelude can never recover: the byte + // stream desynced. Surface it loudly and stop, rather than + // silently stall on bytes we can never drain (which would + // truncate the reply under an already-sent 200). + self.failed = true; + out.push_str(&error_sse("the model response stream could not be decoded")); + break; + } + } + } + self.buf.drain(0..consumed); + out + } + + /// Flush at end of stream. A non-empty buffer here means the upstream closed + /// mid-frame (a truncated or desynced stream), so surface a loud error rather + /// than drop the tail; a clean boundary (empty buffer) emits nothing. + fn finish(&mut self) -> String { + if self.failed || self.buf.is_empty() { + return String::new(); + } + self.failed = true; + error_sse("the model response stream ended before the final frame completed") + } +} + +/// One event-stream message rendered as the Anthropic SSE the client expects. +/// +/// A normal chunk wraps the event as `{"bytes": base64(...)}`. Anything else on an +/// InvokeModelWithResponseStream reply is an exception frame: Bedrock signals +/// mid-stream failures (throttlingException, modelStreamErrorException, +/// internalServerException) this way, with the exception body as the raw payload. +/// Such a frame is surfaced as an Anthropic `error` SSE event rather than dropped, +/// because dropping it would truncate the reply under an already-sent 200 with no +/// error reaching the client. +fn message_to_sse(message: &Message) -> String { + let outer: Option = serde_json::from_slice(message.payload()).ok(); + if let Some(sse) = outer.as_ref().and_then(chunk_to_sse) { + return sse; + } + // Exception / error frame: forward Bedrock's own message so the client sees why. + let message = outer + .as_ref() + .and_then(|o| o.get("message")) + .and_then(Value::as_str) + .unwrap_or("the model returned an error mid-stream"); + error_sse(message) +} + +/// A normal `{"bytes": base64()}` chunk rendered as its SSE line, +/// or `None` if the frame is not a well-formed chunk. +fn chunk_to_sse(outer: &Value) -> Option { + let event_bytes = STANDARD.decode(outer.get("bytes")?.as_str()?).ok()?; + let event: Value = serde_json::from_slice(&event_bytes).ok()?; + let event_type = event.get("type")?.as_str()?; + let data = std::str::from_utf8(&event_bytes).ok()?; + Some(format!("event: {event_type}\ndata: {data}\n\n")) +} + +/// An Anthropic `error` SSE event carrying `message`, so a mid-stream failure +/// reaches the client as a loud error instead of a silently truncated reply. +fn error_sse(message: &str) -> String { + let event = json!({ "type": "error", "error": { "type": "api_error", "message": message } }); + format!("event: error\ndata: {event}\n\n") +} + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use aws_credential_types::provider::SharedCredentialsProvider; + use aws_credential_types::Credentials; + use aws_smithy_eventstream::frame::write_message_to; + use httpmock::prelude::*; + + use super::*; + use crate::creds::{AwsSigV4Cred, BearerTokenCred}; + + fn test_aws_cred() -> AmbientCred { + let creds = Credentials::new( + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + None, + None, + "test", + ); + AmbientCred::Aws(AwsSigV4Cred::with_provider( + "us-east-2", + SharedCredentialsProvider::new(creds), + )) + } + + async fn serve(router: Router) -> String { + let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .expect("bind test server"); + let url = format!("http://{}", listener.local_addr().unwrap()); + tokio::spawn(async move { + axum::serve(listener, router).await.unwrap(); + }); + url + } + + fn aws_route(upstream: &str) -> GatewayRoute { + GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Aws, + region: Some("us-east-2".to_string()), + project: None, + azure_endpoint: None, + cred: test_aws_cred(), + upstream_base_override: Some(upstream.to_string()), + tuned: None, + finetune: None, + } + } + + fn gcp_route(location: &str) -> GatewayRoute { + GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Gcp, + region: Some(location.to_string()), + project: Some("my-proj".to_string()), + azure_endpoint: None, + cred: AmbientCred::Bearer(BearerTokenCred::static_token("t")), + upstream_base_override: None, + tuned: None, + finetune: None, + } + } + + #[test] + fn gcp_vertex_url_regional_vs_global() { + // A region prefixes the host; `global` uses the un-prefixed host. The path always + // carries `locations/{location}`. + let (regional, _) = upstream_target(&gcp_route("us-central1"), Protocol::OpenAi).unwrap(); + assert_eq!( + regional, + "https://us-central1-aiplatform.googleapis.com/v1/projects/my-proj/locations/us-central1/endpoints/openapi/chat/completions" + ); + let (global, _) = upstream_target(&gcp_route("global"), Protocol::OpenAi).unwrap(); + assert_eq!( + global, + "https://aiplatform.googleapis.com/v1/projects/my-proj/locations/global/endpoints/openapi/chat/completions" + ); + } + + #[tokio::test] + async fn vertex_claude_rewrites_body_and_url() { + // Claude on Vertex: the model travels in the URL (as the Vertex `@date` id, + // resolved from Claude Code's dashed spelling), the body carries Vertex's + // version marker instead of a `model`, and the bearer credential rides along. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/v1/projects/my-proj/locations/us-east5/publishers/anthropic/models/claude-haiku-4-5@20251001:rawPredict") + .matches(|req: &HttpMockRequest| { + let body: Value = + serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or(Value::Null); + body.get("model").is_none() + && body["anthropic_version"] == "vertex-2023-10-16" + }) + .matches(|req: &HttpMockRequest| { + req.headers.as_ref().is_some_and(|headers| { + headers.iter().any(|(name, value)| { + name.eq_ignore_ascii_case("authorization") + && value.starts_with("Bearer ") + }) + }) + }); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"msg_1","content":[{"type":"text","text":"pong"}]}"#); + }) + .await; + + let mut route = gcp_route("us-east5"); + route.upstream_base_override = Some(server.base_url()); + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4-5-20251001", + "max_tokens": 16, + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + let text = resp.text().await.unwrap(); + assert!(text.contains("\"pong\""), "upstream body must pass through: {text}"); + mock.assert_async().await; + } + + #[tokio::test] + async fn vertex_claude_streaming_uses_stream_verb() { + // `stream: true` picks the `:streamRawPredict` verb, and Vertex's native + // Anthropic SSE passes through byte-for-byte — no event-stream decode. + let sse = "event: message_start\ndata: {\"type\":\"message_start\"}\n\n\ + event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"; + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/v1/projects/my-proj/locations/us-east5/publishers/anthropic/models/claude-opus-4-8:streamRawPredict"); + then.status(200) + .header("content-type", "text/event-stream") + .body(sse); + }) + .await; + + let mut route = gcp_route("us-east5"); + route.upstream_base_override = Some(server.base_url()); + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-opus-4.8", + "stream": true, + "max_tokens": 16, + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + assert_eq!(resp.headers().get("content-type").unwrap(), "text/event-stream"); + assert_eq!(resp.text().await.unwrap(), sse, "SSE must stream through byte-for-byte"); + mock.assert_async().await; + } + + #[tokio::test] + async fn vertex_claude_rejects_non_boolean_stream() { + // A malformed `stream` picks between two upstream verbs, so it must be a + // loud 400, not a coerced guess. + let mut route = gcp_route("us-east5"); + route.upstream_base_override = Some("http://unused.invalid".to_string()); + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({"model": "claude-opus-4.8", "stream": "yes", "messages": []})) + .send() + .await + .expect("proxy request"); + assert_eq!(resp.status(), 400); + assert!( + resp.text().await.unwrap().contains("GATEWAY_INVALID_REQUEST"), + "must fail on the stream-validation path, not some other 400" + ); + } + + #[tokio::test] + async fn vertex_claude_forwards_allowlisted_betas_as_header() { + // Vertex is the native Messages API: betas ride the standard header, not a + // body field. An allowlisted family crosses over; the OAuth marker every + // wrapped Claude Code session declares does not. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .header("anthropic-beta", "computer-use-2025-01-24") + .matches(|req: &HttpMockRequest| { + let body: Value = + serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or(Value::Null); + // Betas do NOT go in the body for Vertex. + body.get("anthropic_beta").is_none() + }); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"msg_1","content":[]}"#); + }) + .await; + + let mut route = gcp_route("us-east5"); + route.upstream_base_override = Some(server.base_url()); + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .header("anthropic-beta", "computer-use-2025-01-24, oauth-2025-04-20") + .json(&json!({"model": "claude-opus-4.8", "max_tokens": 16, "messages": []})) + .send() + .await + .expect("proxy request"); + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + fn azure_route(endpoint: &str) -> GatewayRoute { + GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Azure, + region: None, + project: None, + azure_endpoint: Some(endpoint.to_string()), + cred: AmbientCred::Bearer(BearerTokenCred::static_token("t")), + upstream_base_override: None, + tuned: None, + finetune: None, + } + } + + #[tokio::test] + async fn foundry_claude_rewrites_model_and_sends_version_header() { + // Claude on Foundry: the model stays in the body, rewritten to the Foundry + // deployment name, on the `/anthropic/v1` path with the version header and + // the bearer credential. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/anthropic/v1/messages") + .header("anthropic-version", "2023-06-01") + .matches(|req: &HttpMockRequest| { + let body: Value = + serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or(Value::Null); + body["model"] == "claude-opus-4-8" + }) + .matches(|req: &HttpMockRequest| { + req.headers.as_ref().is_some_and(|headers| { + headers.iter().any(|(name, value)| { + name.eq_ignore_ascii_case("authorization") + && value.starts_with("Bearer ") + }) + }) + }); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"msg_1","content":[{"type":"text","text":"pong"}]}"#); + }) + .await; + + let url = serve(build_router(vec![azure_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-opus-4.8", + "max_tokens": 16, + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + let text = resp.text().await.unwrap(); + assert!(text.contains("\"pong\""), "upstream body must pass through: {text}"); + mock.assert_async().await; + } + + #[tokio::test] + async fn foundry_claude_forwards_allowlisted_betas_as_header() { + // Foundry takes the standard header; the allowlist still drops the OAuth + // marker so the request is not rejected wholesale. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/anthropic/v1/messages") + .header("anthropic-beta", "computer-use-2025-01-24"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"msg_1","content":[]}"#); + }) + .await; + + let url = serve(build_router(vec![azure_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .header("anthropic-beta", "computer-use-2025-01-24, oauth-2025-04-20") + .json(&json!({"model": "claude-opus-4.8", "max_tokens": 16, "messages": []})) + .send() + .await + .expect("proxy request"); + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn rewrites_model_signs_and_returns_body() { + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/openai/v1/chat/completions") + // The gateway rewrote gpt-oss-20b to the upstream id and injected a credential. + .body_contains("openai.gpt-oss-20b-1:0") + .header_exists("authorization"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"cmpl-1","choices":[{"message":{"content":"pong"}}]}"#); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/chat/completions")) + .json(&json!({"model":"gpt-oss-20b","messages":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + let text = resp.text().await.unwrap(); + assert!(text.contains("\"pong\""), "upstream body must pass through: {text}"); + // The mock only matches when the body carries the rewritten upstream id and an + // Authorization header, so a hit proves the model rewrite and cred injection. + mock.assert_async().await; + } + + fn bedrock_finetune_capability() -> alien_core::bindings::FinetuneCapability { + alien_core::bindings::FinetuneCapability { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_bucket: "my-bucket".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: "support-tuned".to_string(), + job_name: "ai-finetune-llm".to_string(), + role_arn: "arn:aws:iam::123456789012:role/ft".to_string(), + } + } + + #[tokio::test] + async fn submit_finetune_posts_customization_job() { + // POST /finetune submits CreateModelCustomizationJob with the capability's + // names, role, base model, and the S3 training/output URIs, signed for bedrock. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model-customization-jobs") + .body_contains("\"jobName\":\"ai-finetune-llm\"") + .body_contains("\"baseModelIdentifier\":\"amazon.nova-lite-v1:0\"") + .body_contains("s3://my-bucket/training.jsonl") + .body_contains("arn:aws:iam::123456789012:role/ft") + .header_exists("authorization"); + then.status(201) + .header("content-type", "application/json") + .body(r#"{"jobArn":"arn:aws:bedrock:us-east-2:123456789012:model-customization-job/nova.x/abc"}"#); + }) + .await; + + let mut route = aws_route(&server.base_url()); + route.finetune = Some(bedrock_finetune_capability()); + + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/finetune")) + .json(&json!({})) + .send() + .await + .expect("submit request"); + + assert_eq!(resp.status(), 200); + let body: Value = resp.json().await.unwrap(); + assert_eq!(body["servedModel"], "support-tuned"); + assert!(body["jobId"].as_str().unwrap().contains("model-customization-job")); + mock.assert_async().await; + } + + #[tokio::test] + async fn submit_finetune_without_capability_is_rejected() { + // A binding with no finetune capability rejects the submit rather than 500ing. + let server = MockServer::start_async().await; + let route = aws_route(&server.base_url()); // finetune: None + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/finetune")) + .json(&json!({})) + .send() + .await + .expect("submit request"); + assert_eq!(resp.status(), 400); + } + + #[tokio::test] + async fn finetune_status_maps_completed_to_succeeded() { + // GET /finetune/{job} maps Bedrock "Completed" + outputModelArn to succeeded+model. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(GET).path_contains("/model-customization-jobs/"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"status":"Completed","outputModelArn":"arn:aws:bedrock:us-east-2:123456789012:custom-model/nova.x/abc"}"#); + }) + .await; + + let mut route = aws_route(&server.base_url()); + route.finetune = Some(bedrock_finetune_capability()); + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .get(format!("{url}/llm/v1/finetune/job-123")) + .send() + .await + .expect("status request"); + + assert_eq!(resp.status(), 200); + let body: Value = resp.json().await.unwrap(); + assert_eq!(body["status"], "succeeded"); + assert!(body["model"].as_str().unwrap().contains("custom-model")); + mock.assert_async().await; + } + + #[tokio::test] + async fn tuned_model_routes_to_upstream_artifact() { + // A request for the tuned model's public served id must be rewritten to the + // tuned upstream artifact (here a Bedrock custom-model id) and hit the same + // OpenAI chat path — proving the tuned override fires before catalog lookup. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/openai/v1/chat/completions") + .body_contains("custom-model/finance-abc123") + .header_exists("authorization"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"cmpl-1","choices":[{"message":{"content":"pong"}}]}"#); + }) + .await; + + let mut route = aws_route(&server.base_url()); + route.tuned = Some(crate::TunedRoute { + served_id: "finance-model".to_string(), + upstream_id: "custom-model/finance-abc123".to_string(), + }); + + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/chat/completions")) + .json(&json!({"model":"finance-model","messages":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + let text = resp.text().await.unwrap(); + assert!(text.contains("\"pong\""), "upstream body must pass through: {text}"); + mock.assert_async().await; + } + + #[tokio::test] + async fn tuned_model_does_not_shadow_base_catalog() { + // With a tuned model configured, a base catalog id still resolves normally — + // the override only triggers on an exact served-id match. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/openai/v1/chat/completions") + .body_contains("openai.gpt-oss-20b-1:0"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"cmpl-1","choices":[{"message":{"content":"pong"}}]}"#); + }) + .await; + + let mut route = aws_route(&server.base_url()); + route.tuned = Some(crate::TunedRoute { + served_id: "finance-model".to_string(), + upstream_id: "custom-model/finance-abc123".to_string(), + }); + + let url = serve(build_router(vec![route])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/chat/completions")) + .json(&json!({"model":"gpt-oss-20b","messages":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn streams_sse_through_unchanged() { + let sse = "data: {\"choices\":[{\"delta\":{\"content\":\"po\"}}]}\n\n\ + data: {\"choices\":[{\"delta\":{\"content\":\"ng\"}}]}\n\n\ + data: [DONE]\n\n"; + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST).path("/openai/v1/chat/completions"); + then.status(200) + .header("content-type", "text/event-stream") + .body(sse); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/chat/completions")) + .json(&json!({"model":"gpt-oss-20b","stream":true,"messages":[]})) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/event-stream" + ); + let body = resp.text().await.unwrap(); + assert_eq!(body, sse, "SSE must stream through byte-for-byte"); + mock.assert_async().await; + } + + /// Build a single `vnd.amazon.eventstream` frame wrapping `event_json`, the way + /// Bedrock's invoke-with-response-stream does: payload `{"bytes": base64(event)}`. + /// Encoded with aws-smithy-eventstream so the CRCs are real — the decoder + /// validates them. + fn eventstream_frame(event_json: &str) -> Vec { + let payload = format!(r#"{{"bytes":"{}"}}"#, STANDARD.encode(event_json)); + raw_payload_frame(&payload) + } + + /// Build an event-stream frame whose payload is `payload` verbatim, with no + /// `{"bytes": ...}` wrapper — the shape of a Bedrock mid-stream exception frame. + fn raw_payload_frame(payload: &str) -> Vec { + let message = Message::new(Bytes::from(payload.to_string())); + let mut frame = Vec::new(); + write_message_to(&message, &mut frame).expect("encode test frame"); + frame + } + + #[test] + fn decoder_surfaces_bedrock_exception_frame_as_error() { + // A Bedrock mid-stream exception frame's payload is the raw exception JSON, + // NOT wrapped in {"bytes": ...}. It must surface as an Anthropic error event + // rather than be dropped, which would truncate the reply under a 200. + let mut decoder = EventStreamToSse::default(); + let out = decoder.push(&raw_payload_frame(r#"{"message":"Model stream timed out"}"#)); + assert!(out.contains("event: error"), "exception frame must surface an error: {out}"); + assert!( + out.contains("Model stream timed out"), + "the upstream error message must reach the client: {out}" + ); + } + + #[test] + fn decoder_emits_normal_chunk_then_surfaces_a_following_exception() { + let mut decoder = EventStreamToSse::default(); + let mut bytes = + eventstream_frame(r#"{"type":"content_block_delta","delta":{"text":"hi"}}"#); + bytes.extend_from_slice(&raw_payload_frame(r#"{"message":"throttled"}"#)); + let out = decoder.push(&bytes); + assert!(out.contains("event: content_block_delta"), "the normal delta must decode: {out}"); + assert!( + out.contains("event: error") && out.contains("throttled"), + "a trailing exception frame must still surface: {out}" + ); + } + + #[test] + fn decoder_fails_loud_on_desynced_frame() { + // A prelude whose CRC does not match (here: an impossible declared length + // with zeroed CRCs) can never be valid; the decoder must emit an error and + // stop, not silently stall on undrainable bytes. + let mut decoder = EventStreamToSse::default(); + let mut bytes = 8u32.to_be_bytes().to_vec(); // total=8 (<16): impossible + bytes.extend_from_slice(&[0u8; 12]); + let out = decoder.push(&bytes); + assert!(out.contains("event: error"), "a desynced frame must surface an error: {out}"); + // A desync is unrecoverable, so further input is ignored rather than decoded + // mid-stream as if nothing were wrong. + let after = decoder.push(&eventstream_frame(r#"{"type":"message_stop"}"#)); + assert_eq!(after, "", "decoder must stop after a desync"); + } + + #[test] + fn decoder_fails_loud_on_corrupted_frame() { + // A bit-flip inside a valid frame fails the CRC check: the corruption must + // surface as an error, not decode to garbage misattributed to Bedrock. + let mut bytes = eventstream_frame(r#"{"type":"content_block_delta","delta":{"text":"hi"}}"#); + let middle = bytes.len() / 2; + bytes[middle] ^= 0xFF; + let mut decoder = EventStreamToSse::default(); + let out = decoder.push(&bytes); + assert!(out.contains("event: error"), "a corrupted frame must surface an error: {out}"); + } + + #[test] + fn decoder_flushes_incomplete_trailing_frame_as_error() { + // The upstream closed after only part of a frame arrived (a truncated stream); + // finish() must surface a loud error rather than drop the buffered tail. + let mut decoder = EventStreamToSse::default(); + let full = eventstream_frame(r#"{"type":"content_block_delta","delta":{"text":"hi"}}"#); + let partial = &full[..full.len() - 5]; + assert_eq!(decoder.push(partial), "", "an incomplete frame emits nothing until it completes"); + let flushed = decoder.finish(); + assert!( + flushed.contains("event: error"), + "EOF with a buffered partial frame must surface an error: {flushed}" + ); + } + + #[test] + fn decoder_finish_is_silent_on_a_clean_boundary() { + // Every frame consumed: finish() must NOT inject a spurious error event. + let mut decoder = EventStreamToSse::default(); + let out = decoder.push(&eventstream_frame(r#"{"type":"message_stop"}"#)); + assert!(out.contains("event: message_stop")); + assert_eq!(decoder.finish(), "", "a clean stream end must not emit an error"); + } + + #[test] + fn ensure_block_content_normalizes_valid_shapes_and_rejects_the_rest() { + // The same-role fold extends the previous message's content ARRAY, so + // ensure_block_content must yield an array for the two valid shapes — and + // fail loud on a malformed one instead of folding the turn into []. + let mut s = json!({"role": "user", "content": "hi"}); + ensure_block_content(&mut s).expect("string content is valid"); + assert_eq!(s["content"], json!([{"type": "text", "text": "hi"}])); + + let mut arr = json!({"role": "user", "content": [{"type": "text", "text": "x"}]}); + ensure_block_content(&mut arr).expect("array content is valid"); + assert_eq!(arr["content"], json!([{"type": "text", "text": "x"}])); + + let mut missing = json!({"role": "user"}); + ensure_block_content(&mut missing).expect_err("missing content must be rejected"); + + let mut object = json!({"role": "user", "content": {"type": "text", "text": "hi"}}); + ensure_block_content(&mut object).expect_err("object content must be rejected"); + } + + #[test] + fn bedrock_geo_routes_non_us_regions_via_global() { + assert_eq!(bedrock_geo("us-east-2"), "us"); + assert_eq!(bedrock_geo("us-west-2"), "us"); + assert_eq!(bedrock_geo("us-gov-west-1"), "us-gov"); + assert_eq!(bedrock_geo("eu-west-1"), "global"); + assert_eq!(bedrock_geo("ap-southeast-2"), "global"); + assert_eq!(bedrock_geo("ca-central-1"), "global"); + assert_eq!(bedrock_geo("sa-east-1"), "global"); + assert_eq!(bedrock_geo("mx-central-1"), "global"); + } + + #[tokio::test] + async fn claude_streams_through_bedrock_invoke_as_sse() { + // A Claude model on an AWS binding must route to classic InvokeModel — the + // model as a geo inference profile in the URL, no model/stream in the body — + // and the event-stream reply must be decoded back into Anthropic SSE. + let event = r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"pong"}}"#; + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .body_contains("bedrock-2023-05-31") + .header_exists("authorization"); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(event)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({"model":"claude-haiku-4.5","stream":true,"max_tokens":16,"messages":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/event-stream" + ); + let body = resp.text().await.unwrap(); + assert!( + body.contains("event: content_block_delta"), + "event-stream must be decoded to Anthropic SSE: {body}" + ); + assert!(body.contains(r#""text":"pong""#), "delta text must survive: {body}"); + mock.assert_async().await; + } + + #[tokio::test] + async fn claude_non_streaming_returns_json_through_invoke() { + // Without stream, a Claude request must hit the classic InvokeModel `invoke` + // suffix (not invoke-with-response-stream) and its JSON reply passes straight + // through untouched — the event-stream decoder is only for the streaming path. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke") + .body_contains("bedrock-2023-05-31") + .header_exists("authorization"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"type":"message","content":[{"type":"text","text":"pong"}]}"#); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "max_tokens": 16, + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "application/json" + ); + let text = resp.text().await.unwrap(); + assert!(text.contains(r#""pong""#), "non-streaming JSON must pass through: {text}"); + mock.assert_async().await; + } + + #[tokio::test] + async fn claude_stream_truncated_midframe_surfaces_error_to_client() { + // The upstream sends a complete HTTP body that ends mid event-stream frame + // (a truncated stream). End to end, the client must receive an `event: error` + // rather than a stream that just stops. This exercises the real + // Body::from_stream + unfold finish() plumbing that the decoder unit tests + // (which call finish() directly) do not cover. + let full = eventstream_frame(r#"{"type":"content_block_delta","delta":{"text":"partial"}}"#); + let truncated = full[..full.len() - 6].to_vec(); // an incomplete final frame + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST).path( + "/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream", + ); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(truncated); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({"model":"claude-haiku-4.5","stream":true,"max_tokens":16,"messages":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + let body = resp.text().await.unwrap(); + assert!( + body.contains("event: error"), + "a truncated upstream stream must surface an error to the client: {body}" + ); + mock.assert_async().await; + } + + #[tokio::test] + async fn bedrock_path_drops_fields_bedrock_rejects() { + // A latest Claude Code body carries newer Anthropic fields that Bedrock's + // classic schema rejects; the gateway must strip them (the mock only matches, + // and thus 200s, when they are absent from the upstream body). + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .matches(|req: &HttpMockRequest| { + let body = req + .body + .as_deref() + .map(String::from_utf8_lossy) + .unwrap_or_default(); + !body.contains("output_config") + && !body.contains("context_management") + && !body.contains("adaptive") + }); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(r#"{"type":"message_stop"}"#)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 16, + "output_config": {"effort": "xhigh"}, + "context_management": {"edits": []}, + "thinking": {"type": "adaptive"}, + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn bedrock_path_drops_server_tools_it_cannot_host() { + // Anthropic *server*-executed tools (advisor, web search) run on Anthropic's + // API servers; Bedrock rejects their tags. Client tools and the + // client-executed types Bedrock DOES host (text editor, computer use, …) + // must survive untouched. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .matches(|req: &HttpMockRequest| { + let body: Value = serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or_default(); + let tools = body["tools"].as_array().cloned().unwrap_or_default(); + // read_file + text_editor + computer survive; advisor + web_search drop. + tools.len() == 3 + && tools[0]["name"] == "read_file" + // `defer_loading` stripped from the surviving client tool. + && tools[0].get("defer_loading").is_none() + && tools[1]["type"] == "text_editor_20250728" + && tools[2]["type"] == "computer_20250124" + }); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(r#"{"type":"message_stop"}"#)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 16, + "tools": [ + {"name": "read_file", "description": "reads", "input_schema": {"type": "object"}, "defer_loading": true}, + {"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}, + {"type": "computer_20250124", "name": "computer"}, + {"type": "advisor_20260301", "name": "advisor"}, + {"type": "web_search_20250305", "name": "web_search"} + ], + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn bedrock_path_bridges_the_anthropic_beta_header_into_the_body() { + // Clients declare betas as the `anthropic-beta` HTTP header, but classic + // InvokeModel reads only the body's `anthropic_beta`. Without the bridge, a + // forwarded beta-gated tool (computer_*) reaches Bedrock with no beta and 400s. + // A body-declared beta must survive alongside the bridged one, and header tags + // outside BEDROCK_BETA_PREFIXES (`oauth-2025-04-20` is on every OAuth Claude + // Code request; Bedrock rejects it as "invalid beta flag") must NOT be bridged. + // The mock only matches — and so only 200s — when the kept betas and the tool + // arrived and the rejected tags did not. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .matches(|req: &HttpMockRequest| { + let body: Value = serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or_default(); + let betas = body["anthropic_beta"].as_array().cloned().unwrap_or_default(); + let tools = body["tools"].as_array().cloned().unwrap_or_default(); + betas.iter().any(|b| b == "context-management-2025-06-27") + && betas.iter().any(|b| b == "computer-use-2025-01-24") + && !betas.iter().any(|b| b == "oauth-2025-04-20") + && !betas.iter().any(|b| b == "tool-search-2025-10-02") + && tools.len() == 1 + && tools[0]["type"] == "computer_20250124" + }); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(r#"{"type":"message_stop"}"#)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .header( + "anthropic-beta", + "computer-use-2025-01-24,oauth-2025-04-20,tool-search-2025-10-02", + ) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 16, + "anthropic_beta": ["context-management-2025-06-27"], + "tools": [{"type": "computer_20250124", "name": "computer"}], + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn bedrock_path_downgrades_system_role_messages() { + // Claude Code (mid-conversation-system beta) puts `role:"system"` turns + // inside `messages`; Bedrock's pinned schema allows only user/assistant + // there and enforces alternation. The gateway must re-tag the turn as + // `user` in place and fold it into its same-role neighbor. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .matches(|req: &HttpMockRequest| { + let body: Value = serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or_default(); + let messages = body["messages"].as_array().cloned().unwrap_or_default(); + // One merged user turn: original user text + the downgraded + // system turn's text, in conversation order. + messages.len() == 1 + && messages[0]["role"] == "user" + && messages[0]["content"][0]["text"] == "hi" + && messages[0]["content"][1]["text"] == "hook output" + }); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(r#"{"type":"message_stop"}"#)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 16, + "messages": [ + {"role": "user", "content": "hi"}, + {"role": "system", "content": "hook output"} + ] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn bedrock_path_folds_tool_results_ahead_of_downgraded_system_text() { + // A hook can emit a system turn between a tool call and its result: + // [assistant(tool_use), system(text), user(tool_result)]. The fold merges + // the downgraded system turn with the tool_result turn — and the result + // block must lead the merged message (live-verified: Bedrock rejects + // `[text, tool_result]` with "'tool_use' ids were found without + // 'tool_result' blocks immediately after"). + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .matches(|req: &HttpMockRequest| { + let body: Value = serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or_default(); + let messages = body["messages"].as_array().cloned().unwrap_or_default(); + messages.len() == 3 + && messages[2]["role"] == "user" + && messages[2]["content"][0]["type"] == "tool_result" + && messages[2]["content"][1]["text"] == "hook output" + }); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(r#"{"type":"message_stop"}"#)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 16, + "messages": [ + {"role": "user", "content": "What time is it?"}, + {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_time", "input": {}}]}, + {"role": "system", "content": "hook output"}, + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_01", "content": "12:00"}]} + ] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn bedrock_path_drops_tool_choice_with_the_last_server_tool() { + // When every declared tool is server-executed, stripping them leaves + // `tools: []` plus a tool_choice forcing a tool that no longer exists — + // both of which Bedrock rejects outright. The whole pair must go. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .matches(|req: &HttpMockRequest| { + let body: Value = serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or_default(); + body.get("tools").is_none() && body.get("tool_choice").is_none() + }); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(r#"{"type":"message_stop"}"#)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 16, + "tools": [{"type": "web_search_20250305", "name": "web_search"}], + "tool_choice": {"type": "tool", "name": "web_search"}, + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn bedrock_path_rejects_a_non_boolean_stream_flag() { + // `stream` chooses between two upstream endpoints; a malformed value used + // to be coerced to `false`, answering an SSE client with a JSON body it + // reads as a hang. It must be a loud 400 instead. + let server = MockServer::start_async().await; + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": "true", + "max_tokens": 16, + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 400); + let body = resp.text().await.expect("response body"); + assert!( + body.contains("GATEWAY_INVALID_REQUEST"), + "the 400 must be the gateway's own validation error: {body}" + ); + } + + #[tokio::test] + async fn bedrock_path_keeps_a_string_form_body_beta_alongside_header_betas() { + // The body's `anthropic_beta` can be a single string; merging header + // betas must keep that string form so a beta the client set on the body + // still reaches Bedrock alongside the header betas. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/model/us.anthropic.claude-haiku-4-5-20251001-v1:0/invoke-with-response-stream") + .matches(|req: &HttpMockRequest| { + let body: Value = serde_json::from_slice(req.body.as_deref().unwrap_or_default()) + .unwrap_or_default(); + let betas = body["anthropic_beta"].as_array().cloned().unwrap_or_default(); + betas.iter().any(|b| b == "context-management-2025-06-27") + && betas.iter().any(|b| b == "computer-use-2025-01-24") + }); + then.status(200) + .header("content-type", "application/vnd.amazon.eventstream") + .body(eventstream_frame(r#"{"type":"message_stop"}"#)); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/messages")) + .header("anthropic-beta", "computer-use-2025-01-24") + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 16, + "anthropic_beta": "context-management-2025-06-27", + "messages": [{"role": "user", "content": "hi"}] + })) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + mock.assert_async().await; + } + + #[tokio::test] + async fn responses_pass_through_to_mantle() { + // Codex's /v1/responses must forward byte-for-byte to the mantle Responses + // endpoint with the model id rewritten and a SigV4 credential attached, and + // the Responses SSE must come back unchanged. + let sse = "data: {\"type\":\"response.output_text.delta\",\"delta\":\"po\"}\n\n\ + data: {\"type\":\"response.completed\"}\n\n"; + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(POST) + .path("/v1/responses") + // Mantle's Responses id drops the chat endpoint's version suffix. + .body_contains("\"openai.gpt-oss-20b\"") + // The SigV4 credential must be scoped to the bedrock-mantle service, + // not plain bedrock: mantle rejects a signature scoped to the wrong + // service. The scope segment appears verbatim in the credential. + .matches(|req: &HttpMockRequest| { + req.headers.as_ref().is_some_and(|headers| { + headers.iter().any(|(name, value)| { + name.eq_ignore_ascii_case("authorization") + && value.contains("/bedrock-mantle/") + }) + }) + }); + then.status(200) + .header("content-type", "text/event-stream") + .body(sse); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/responses")) + .json(&json!({"model":"gpt-oss-20b","stream":true,"input":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("proxy request"); + + assert_eq!(resp.status(), 200); + assert_eq!(resp.text().await.unwrap(), sse, "Responses SSE must pass through byte-for-byte"); + mock.assert_async().await; + } + + #[tokio::test] + async fn claude_over_responses_is_404() { + // Claude on mantle is Messages-only; a Claude id over /v1/responses must be + // rejected by the gateway, not forwarded. + let server = MockServer::start_async().await; + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/responses")) + .json(&json!({"model":"claude-haiku-4.5","input":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("proxy request"); + assert_eq!(resp.status(), 404); + // The mock upstream answers unmatched requests with its own 404, so the + // status alone cannot prove the gateway rejected the model rather than + // forwarding the request — the body must carry the gateway's error code. + let body = resp.text().await.expect("response body"); + assert!( + body.contains("GATEWAY_MODEL_NOT_AVAILABLE"), + "the 404 must be the gateway's own rejection, not a forwarded upstream 404: {body}" + ); + } + + #[tokio::test] + async fn unknown_model_is_404() { + let server = MockServer::start_async().await; + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + let resp = reqwest::Client::new() + .post(format!("{url}/llm/v1/chat/completions")) + .json(&json!({"model":"not-a-real-model","messages":[]})) + .send() + .await + .expect("proxy request"); + assert_eq!(resp.status(), 404); + let body = resp.text().await.expect("response body"); + assert!( + body.contains("GATEWAY_MODEL_NOT_AVAILABLE"), + "the 404 must be the gateway's own rejection, not a forwarded upstream 404: {body}" + ); + } + + #[tokio::test] + async fn models_lists_the_clouds_catalog() { + let url = serve(build_router(vec![aws_route("https://unused.example")])).await; + let resp = reqwest::get(format!("{url}/llm/v1/models")).await.unwrap(); + assert_eq!(resp.status(), 200); + let body: Value = resp.json().await.unwrap(); + assert_eq!(body["object"], "list"); + let ids: Vec<&str> = body["data"] + .as_array() + .unwrap() + .iter() + .map(|m| m["id"].as_str().unwrap()) + .collect(); + assert!(ids.contains(&"gpt-oss-20b"), "AWS catalog must include gpt-oss-20b: {ids:?}"); + assert!(ids.contains(&"claude-opus-4.8"), "AWS catalog must include Claude: {ids:?}"); + } +} diff --git a/crates/alien-gateway/tests/integration.rs b/crates/alien-gateway/tests/integration.rs new file mode 100644 index 000000000..9a0380ea7 --- /dev/null +++ b/crates/alien-gateway/tests/integration.rs @@ -0,0 +1,159 @@ +//! End-to-end gateway routing across two clouds with mocked upstreams. +//! +//! Builds the real router with an AWS binding and an Azure binding pointed at two +//! mock upstream servers, then drives requests through the running loopback server +//! and asserts each is routed to the right upstream with the model id rewritten (per +//! the alien-core catalog), an ambient auth header injected, and the body streamed +//! back unchanged. Credentials are static (no metadata/network) so the test is +//! hermetic; the live ambient-credential resolution is exercised separately. + +use std::net::Ipv4Addr; + +use alien_core::Platform; +use alien_gateway::{build_router, AmbientCred, AwsSigV4Cred, BearerTokenCred, GatewayRoute}; +use aws_credential_types::provider::SharedCredentialsProvider; +use aws_credential_types::Credentials; +use httpmock::prelude::*; +use serde_json::{json, Value}; + +fn aws_cred() -> AmbientCred { + let creds = Credentials::new( + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + None, + None, + "test", + ); + AmbientCred::Aws(AwsSigV4Cred::with_provider( + "us-east-2", + SharedCredentialsProvider::new(creds), + )) +} + +async fn serve(router: axum::Router) -> String { + let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .expect("bind test server"); + let url = format!("http://{}", listener.local_addr().unwrap()); + tokio::spawn(async move { + axum::serve(listener, router).await.unwrap(); + }); + url +} + +#[tokio::test] +async fn routes_two_clouds_with_rewrite_auth_and_passthrough() { + let aws_upstream = MockServer::start_async().await; + let aws_mock = aws_upstream + .mock_async(|when, then| { + when.method(POST) + .path("/openai/v1/chat/completions") + // Rewritten to the upstream id, and SigV4-signed. + .body_contains("openai.gpt-oss-20b-1:0") + .header_exists("authorization"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"aws","choices":[{"message":{"content":"aws-pong"}}]}"#); + }) + .await; + + let azure_upstream = MockServer::start_async().await; + let azure_mock = azure_upstream + .mock_async(|when, then| { + when.method(POST) + .path("/openai/v1/chat/completions") + .body_contains("gpt-4.1") + // The static bearer token is injected verbatim. + .header("authorization", "Bearer test-azure-token"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"az","choices":[{"message":{"content":"az-pong"}}]}"#); + }) + .await; + + let routes = vec![ + GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Aws, + region: Some("us-east-2".to_string()), + project: None, + azure_endpoint: None, + cred: aws_cred(), + upstream_base_override: Some(aws_upstream.base_url()), + tuned: None, + finetune: None, + }, + GatewayRoute { + name: "azllm".to_string(), + cloud: Platform::Azure, + region: None, + project: None, + azure_endpoint: Some(azure_upstream.base_url()), + cred: AmbientCred::Bearer(BearerTokenCred::static_token("test-azure-token")), + upstream_base_override: Some(azure_upstream.base_url()), + tuned: None, + finetune: None, + }, + ]; + + let base = serve(build_router(routes)).await; + let client = reqwest::Client::new(); + + // AWS binding: gpt-oss-20b -> openai.gpt-oss-20b-1:0 on the AWS upstream. + let aws_resp = client + .post(format!("{base}/llm/v1/chat/completions")) + .json(&json!({"model":"gpt-oss-20b","messages":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("aws request"); + assert_eq!(aws_resp.status(), 200); + assert!(aws_resp.text().await.unwrap().contains("aws-pong")); + aws_mock.assert_async().await; + + // Azure binding: gpt-4.1 on the Azure upstream with the bearer token. + let az_resp = client + .post(format!("{base}/azllm/v1/chat/completions")) + .json(&json!({"model":"gpt-4.1","messages":[{"role":"user","content":"hi"}]})) + .send() + .await + .expect("azure request"); + assert_eq!(az_resp.status(), 200); + assert!(az_resp.text().await.unwrap().contains("az-pong")); + azure_mock.assert_async().await; + + // Each binding lists its own cloud's curated catalog. + let aws_models: Value = client + .get(format!("{base}/llm/v1/models")) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + let aws_ids: Vec<&str> = aws_models["data"] + .as_array() + .unwrap() + .iter() + .map(|m| m["id"].as_str().unwrap()) + .collect(); + assert!(aws_ids.contains(&"gpt-oss-20b")); + assert!(aws_ids.contains(&"claude-opus-4.8")); + assert!(!aws_ids.contains(&"gpt-4.1"), "AWS catalog must not list the Azure model"); + + let az_models: Value = client + .get(format!("{base}/azllm/v1/models")) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + let az_ids: Vec<&str> = az_models["data"] + .as_array() + .unwrap() + .iter() + .map(|m| m["id"].as_str().unwrap()) + .collect(); + assert!(az_ids.contains(&"gpt-4.1")); + assert!(!az_ids.contains(&"gpt-oss-20b"), "Azure catalog must not list the AWS model"); +} diff --git a/crates/alien-gateway/tests/launcher.rs b/crates/alien-gateway/tests/launcher.rs new file mode 100644 index 000000000..e1e521e3a --- /dev/null +++ b/crates/alien-gateway/tests/launcher.rs @@ -0,0 +1,59 @@ +//! Integration tests for the `alien-ai-gateway` container launcher binary. +//! +//! These cover the passthrough paths (no cloud credentials needed). Credential +//! injection is exercised only by the end-to-end cloud tests; the lib's +//! `gateway_starts_and_serves_health` covers startup and health on empty bindings. + +use std::process::Command; + +// With no ALIEN_*_BINDING for an AI resource, the launcher must exec the app +// unchanged and must NOT set ALIEN_AI_GATEWAY_URL. +#[test] +fn passthrough_execs_app_without_gateway_when_no_ai_binding() { + let exe = env!("CARGO_BIN_EXE_alien-ai-gateway"); + // env_clear so a stray ALIEN_*_BINDING in the dev/CI env can't flip this into a + // gateway-spawn path. /bin/sh is an absolute path, so no PATH is needed. + let out = Command::new(exe) + .env_clear() + .args(["--", "/bin/sh", "-c", "printf '%s' \"gw=${ALIEN_AI_GATEWAY_URL:-unset}\""]) + .output() + .expect("launcher should run"); + assert!( + out.status.success(), + "stderr: {}", + String::from_utf8_lossy(&out.stderr) + ); + assert_eq!(String::from_utf8_lossy(&out.stdout), "gw=unset"); +} + +// A malformed invocation (no `--` separator, no command) fails fast, non-zero. +#[test] +fn missing_command_fails_fast() { + let exe = env!("CARGO_BIN_EXE_alien-ai-gateway"); + let out = Command::new(exe).output().expect("launcher should run"); + assert!(!out.status.success()); + assert!(String::from_utf8_lossy(&out.stderr).contains("alien-ai-gateway")); +} + +// A BYO-key (External) binding is not served by the gateway; the launcher must +// treat it as "no gateway" and exec the app directly (the SDK uses the key). +#[test] +fn external_binding_is_passthrough() { + let exe = env!("CARGO_BIN_EXE_alien-ai-gateway"); + // env_clear so only the External binding under test is present. + let out = Command::new(exe) + .env_clear() + .args(["--", "/bin/sh", "-c", "printf '%s' \"gw=${ALIEN_AI_GATEWAY_URL:-unset}\""]) + .env( + "ALIEN_LLM_BINDING", + r#"{"service":"external","provider":"openai","apiKey":"sk-x"}"#, + ) + .output() + .expect("launcher should run"); + assert!( + out.status.success(), + "stderr: {}", + String::from_utf8_lossy(&out.stderr) + ); + assert_eq!(String::from_utf8_lossy(&out.stdout), "gw=unset"); +} diff --git a/crates/alien-gateway/tests/live_bedrock.rs b/crates/alien-gateway/tests/live_bedrock.rs new file mode 100644 index 000000000..c52aa2a3b --- /dev/null +++ b/crates/alien-gateway/tests/live_bedrock.rs @@ -0,0 +1,152 @@ +//! Live verification against real AWS Bedrock. Ignored by default — it makes a +//! real inference call and needs ambient AWS credentials in the environment. +//! +//! Run it with: +//! eval "$(aws configure export-credentials --profile

--format env)" +//! cargo test -p alien-gateway --test live_bedrock -- --ignored --nocapture +//! +//! This is the end-to-end proof that the Rust SigV4 signer produces a signature +//! Bedrock accepts: the gateway rewrites the public model id, signs with the SDK +//! default credential chain (service `bedrock`), and forwards to the real +//! `/openai/v1/chat/completions` endpoint — no static key, no mock. + +use std::net::Ipv4Addr; + +use alien_core::Platform; +use alien_gateway::{build_router, AmbientCred, AwsSigV4Cred, GatewayRoute}; +use serde_json::{json, Value}; + +async fn serve(router: axum::Router) -> String { + let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .expect("bind test server"); + let url = format!("http://{}", listener.local_addr().unwrap()); + tokio::spawn(async move { + axum::serve(listener, router).await.unwrap(); + }); + url +} + +#[tokio::test] +#[ignore = "hits real AWS Bedrock; needs ambient AWS credentials in the environment"] +async fn live_bedrock_openai_chat() { + let cred = AmbientCred::Aws( + AwsSigV4Cred::new("us-east-2") + .await + .expect("resolve AWS credentials from the default chain"), + ); + let route = GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Aws, + region: Some("us-east-2".to_string()), + project: None, + azure_endpoint: None, + cred, + upstream_base_override: None, + tuned: None, + finetune: None, + }; + + let base = serve(build_router(vec![route])).await; + + let resp = reqwest::Client::new() + .post(format!("{base}/llm/v1/chat/completions")) + .json(&json!({ + "model": "gpt-oss-20b", + "messages": [{ "role": "user", "content": "Reply with exactly: pong" }], + "max_completion_tokens": 1024, + "reasoning_effort": "low" + })) + .send() + .await + .expect("request to the gateway"); + + let status = resp.status(); + let body: Value = resp.json().await.expect("gateway response should be JSON"); + eprintln!("live bedrock status={status} body={body}"); + + assert!( + status.is_success(), + "Bedrock must accept the Rust-signed request; got {status}: {body}" + ); + let content = body["choices"][0]["message"]["content"] + .as_str() + .unwrap_or_default(); + assert!( + content.to_lowercase().contains("pong"), + "expected a 'pong' completion, got: {content:?}" + ); +} + +#[tokio::test] +#[ignore = "hits real AWS Bedrock Claude via classic InvokeModel; needs a console model grant + ambient AWS credentials"] +async fn live_bedrock_claude_streaming() { + // The end-to-end proof for the Claude path: classic Bedrock InvokeModel with + // response streaming, whose event-stream frames the gateway decodes back into + // Anthropic SSE. Unlike the mock tests (which replay only the frames we chose), + // this exercises the decoder's frame-shape classification against Bedrock's REAL + // frame sequence — the one thing that reveals whether a benign non-chunk frame + // would be misclassified as an error and truncate a healthy stream. + let cred = AmbientCred::Aws( + AwsSigV4Cred::new("us-east-2") + .await + .expect("resolve AWS credentials from the default chain"), + ); + let route = GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Aws, + region: Some("us-east-2".to_string()), + project: None, + azure_endpoint: None, + cred, + upstream_base_override: None, + tuned: None, + finetune: None, + }; + + let base = serve(build_router(vec![route])).await; + + let resp = reqwest::Client::new() + .post(format!("{base}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "stream": true, + "max_tokens": 64, + "messages": [{ "role": "user", "content": "Reply with exactly: pong" }] + })) + .send() + .await + .expect("request to the gateway"); + + let status = resp.status(); + let body = resp.text().await.expect("gateway response body"); + eprintln!("live claude stream status={status}\n{body}"); + + assert!( + status.is_success(), + "Claude via classic Bedrock must accept the request; got {status}: {body}" + ); + assert!( + body.contains("event: content_block_delta") || body.contains("\"text\""), + "expected decoded Anthropic SSE deltas: {body}" + ); + // A healthy stream must NOT trip the decoder's error branch: if it does, the + // payload-shape frame heuristic misclassified a real Bedrock frame. + assert!( + !body.contains("event: error"), + "a healthy Claude stream must not surface a decoder error: {body}" + ); + // The reply text streams as separate text_delta events ("p" then "ong"), so + // reconstruct it before asserting rather than expecting a contiguous substring. + let reply: String = body + .lines() + .filter_map(|line| line.strip_prefix("data:")) + .filter_map(|data| serde_json::from_str::(data.trim()).ok()) + .filter(|event| event["type"] == "content_block_delta") + .filter_map(|event| event["delta"]["text"].as_str().map(str::to_owned)) + .collect(); + assert!( + reply.to_lowercase().contains("pong"), + "expected a 'pong' completion; reconstructed {reply:?} from stream: {body}" + ); +} diff --git a/crates/alien-gateway/tests/live_foundry_claude.rs b/crates/alien-gateway/tests/live_foundry_claude.rs new file mode 100644 index 000000000..c4a923b75 --- /dev/null +++ b/crates/alien-gateway/tests/live_foundry_claude.rs @@ -0,0 +1,114 @@ +//! Live verification of Claude on Azure Foundry. Ignored by default — it makes a +//! real inference call and needs a Foundry endpoint + Entra token in the +//! environment. +//! +//! Run it with: +//! export AZURE_AI_ENDPOINT="https://.cognitiveservices.azure.com/" +//! export AZURE_ACCESS_TOKEN="$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)" +//! cargo test -p alien-gateway --test live_foundry_claude -- --ignored --nocapture +//! +//! Besides proving the arm end-to-end, this settles the host/audience question the +//! code left open. AZURE_AI_ENDPOINT must be the account endpoint the AiBinding +//! carries in production (the AIServices account's `properties.endpoint`, the +//! `cognitiveservices.azure.com` shape) — a green run against the +//! `services.ai.azure.com` host would validate a host production bindings never +//! use. Probe order: (1) the binding-carried host with the `ai.azure.com` token +//! above; (2) on 404, the `services.ai.azure.com` host — meaning the arm needs a +//! host derivation; (3) on 401, retry the token with +//! `--resource https://cognitiveservices.azure.com` — meaning the audience swap +//! is unnecessary. Record which combination Foundry accepted. + +use std::net::Ipv4Addr; + +use alien_core::Platform; +use alien_gateway::{build_router, AmbientCred, BearerTokenCred, GatewayRoute}; +use serde_json::{json, Value}; + +async fn serve(router: axum::Router) -> String { + let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .expect("bind test server"); + let url = format!("http://{}", listener.local_addr().unwrap()); + tokio::spawn(async move { + axum::serve(listener, router).await.unwrap(); + }); + url +} + +fn foundry_route() -> GatewayRoute { + let endpoint = + std::env::var("AZURE_AI_ENDPOINT").expect("AZURE_AI_ENDPOINT must name the account"); + let token = + std::env::var("AZURE_ACCESS_TOKEN").expect("AZURE_ACCESS_TOKEN must hold an Entra token"); + GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Azure, + region: None, + project: None, + azure_endpoint: Some(endpoint), + cred: AmbientCred::Bearer(BearerTokenCred::static_token(token)), + upstream_base_override: None, + tuned: None, + finetune: None, + } +} + +#[tokio::test] +#[ignore = "hits real Foundry Claude; needs AZURE_AI_ENDPOINT + AZURE_ACCESS_TOKEN and a Claude deployment on the resource"] +async fn live_foundry_claude_messages() { + let base = serve(build_router(vec![foundry_route()])).await; + + let resp = reqwest::Client::new() + .post(format!("{base}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "max_tokens": 64, + "messages": [{ "role": "user", "content": "Reply with exactly: pong" }] + })) + .send() + .await + .expect("request to the gateway"); + + let status = resp.status(); + let text = resp.text().await.expect("gateway response body"); + eprintln!("live foundry claude status={status} body={text}"); + let body: Value = serde_json::from_str(&text).expect("gateway response should be JSON"); + + assert!( + status.is_success(), + "Foundry must accept the Messages request; got {status}: {body}" + ); + let content = body["content"][0]["text"].as_str().unwrap_or_default(); + assert!( + content.to_lowercase().contains("pong"), + "expected a 'pong' reply, got: {content:?}" + ); +} + +#[tokio::test] +#[ignore = "hits real Foundry Claude streaming; needs AZURE_AI_ENDPOINT + AZURE_ACCESS_TOKEN and a Claude deployment on the resource"] +async fn live_foundry_claude_streaming() { + // Standard Anthropic streaming on the body's `stream` flag; the reply must be + // native Anthropic SSE passed through untouched. + let base = serve(build_router(vec![foundry_route()])).await; + + let resp = reqwest::Client::new() + .post(format!("{base}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "max_tokens": 64, + "stream": true, + "messages": [{ "role": "user", "content": "Reply with exactly: pong" }] + })) + .send() + .await + .expect("request to the gateway"); + + let status = resp.status(); + assert!(status.is_success(), "Foundry streaming must return 2xx; got {status}"); + let body = resp.text().await.expect("stream body"); + let head: String = body.chars().take(400).collect(); + eprintln!("live foundry claude stream head: {head}"); + assert!(body.contains("message_start"), "SSE must open with message_start: {body}"); + assert!(body.contains("message_stop"), "SSE must close with message_stop: {body}"); +} diff --git a/crates/alien-gateway/tests/live_vertex_claude.rs b/crates/alien-gateway/tests/live_vertex_claude.rs new file mode 100644 index 000000000..1b65fb4b9 --- /dev/null +++ b/crates/alien-gateway/tests/live_vertex_claude.rs @@ -0,0 +1,106 @@ +//! Live verification of Claude on GCP Vertex. Ignored by default — it makes a +//! real inference call and needs a project + access token in the environment. +//! +//! Run it with: +//! export GCP_PROJECT= +//! export GCP_ACCESS_TOKEN="$(gcloud auth print-access-token)" +//! cargo test -p alien-gateway --test live_vertex_claude -- --ignored --nocapture +//! +//! This is the end-to-end proof for the Vertex Claude arm: the gateway moves the +//! model id into the `:rawPredict` URL, injects the Vertex version marker, and the +//! project's Model Garden entitlement accepts the call — the one thing code +//! reading cannot establish. + +use std::net::Ipv4Addr; + +use alien_core::Platform; +use alien_gateway::{build_router, AmbientCred, BearerTokenCred, GatewayRoute}; +use serde_json::{json, Value}; + +async fn serve(router: axum::Router) -> String { + let listener = tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .expect("bind test server"); + let url = format!("http://{}", listener.local_addr().unwrap()); + tokio::spawn(async move { + axum::serve(listener, router).await.unwrap(); + }); + url +} + +fn vertex_route() -> GatewayRoute { + let project = std::env::var("GCP_PROJECT").expect("GCP_PROJECT must name the target project"); + let token = + std::env::var("GCP_ACCESS_TOKEN").expect("GCP_ACCESS_TOKEN must hold a bearer token"); + GatewayRoute { + name: "llm".to_string(), + cloud: Platform::Gcp, + region: Some("global".to_string()), + project: Some(project), + azure_endpoint: None, + cred: AmbientCred::Bearer(BearerTokenCred::static_token(token)), + upstream_base_override: None, + tuned: None, + finetune: None, + } +} + +#[tokio::test] +#[ignore = "hits real Vertex Claude; needs GCP_PROJECT + GCP_ACCESS_TOKEN and a Model Garden grant"] +async fn live_vertex_claude_messages() { + let base = serve(build_router(vec![vertex_route()])).await; + + let resp = reqwest::Client::new() + .post(format!("{base}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "max_tokens": 64, + "messages": [{ "role": "user", "content": "Reply with exactly: pong" }] + })) + .send() + .await + .expect("request to the gateway"); + + let status = resp.status(); + let text = resp.text().await.expect("gateway response body"); + eprintln!("live vertex claude status={status} body={text}"); + let body: Value = serde_json::from_str(&text).expect("gateway response should be JSON"); + + assert!( + status.is_success(), + "Vertex must accept the rawPredict request; got {status}: {body}" + ); + let content = body["content"][0]["text"].as_str().unwrap_or_default(); + assert!( + content.to_lowercase().contains("pong"), + "expected a 'pong' reply, got: {content:?}" + ); +} + +#[tokio::test] +#[ignore = "hits real Vertex Claude streaming; needs GCP_PROJECT + GCP_ACCESS_TOKEN and a Model Garden grant"] +async fn live_vertex_claude_streaming() { + // Streaming picks the `:streamRawPredict` verb and must come back as native + // Anthropic SSE — message_start through message_stop, passed through untouched. + let base = serve(build_router(vec![vertex_route()])).await; + + let resp = reqwest::Client::new() + .post(format!("{base}/llm/v1/messages")) + .json(&json!({ + "model": "claude-haiku-4.5", + "max_tokens": 64, + "stream": true, + "messages": [{ "role": "user", "content": "Reply with exactly: pong" }] + })) + .send() + .await + .expect("request to the gateway"); + + let status = resp.status(); + assert!(status.is_success(), "Vertex streaming must return 2xx; got {status}"); + let body = resp.text().await.expect("stream body"); + let head: String = body.chars().take(400).collect(); + eprintln!("live vertex claude stream head: {head}"); + assert!(body.contains("message_start"), "SSE must open with message_start: {body}"); + assert!(body.contains("message_stop"), "SSE must close with message_stop: {body}"); +} diff --git a/crates/alien-gcp-clients/src/gcp/aiplatform.rs b/crates/alien-gcp-clients/src/gcp/aiplatform.rs new file mode 100644 index 000000000..509a53ba2 --- /dev/null +++ b/crates/alien-gcp-clients/src/gcp/aiplatform.rs @@ -0,0 +1,406 @@ +//! Vertex AI Platform client covering the tuning-job surface used for +//! fine-tuning. +//! +//! Only the two calls the fine-tuning controller needs are exposed: +//! `create_tuning_job` (POST `.../tuningJobs`) and `get_tuning_job` +//! (GET `.../tuningJobs/{id}`). See: +//! +//! +//! Unlike the other GCP services, Vertex AI is *regional*: the host is +//! `{location}-aiplatform.googleapis.com`. Because [`GcpServiceConfig::base_url`] +//! must return a `&'static str`, the location-specific base URL is built in the +//! client constructor and injected as a service override, so [`GcpClientBase`]'s +//! standard override lookup resolves it. + +use crate::gcp::api_client::{GcpClientBase, GcpServiceConfig}; +use crate::gcp::longrunning::Status; +use crate::gcp::GcpClientConfig; +use crate::gcp::GcpClientConfigExt; +use crate::gcp::ServiceOverrides; +use alien_client_core::Result; +use bon::Builder; +use reqwest::{Client, Method}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test-utils")] +use mockall::automock; + +/// Service key used to inject the regional base-URL override for Vertex AI. +const AIPLATFORM_SERVICE_KEY: &str = "aiplatform"; + +/// Vertex AI (aiplatform) service configuration. +/// +/// The `base_url` returned here is a placeholder for the global host; the real, +/// region-scoped host is always supplied via a service override installed by +/// [`AiPlatformClient::new`], so this default is never used in practice. +#[derive(Debug)] +pub struct AiPlatformServiceConfig; + +impl GcpServiceConfig for AiPlatformServiceConfig { + fn base_url(&self) -> &'static str { + // Overridden per-region in the constructor; kept valid as a fallback. + "https://aiplatform.googleapis.com/v1" + } + + fn default_audience(&self) -> &'static str { + "https://aiplatform.googleapis.com/" + } + + fn service_name(&self) -> &'static str { + "Vertex AI" + } + + fn service_key(&self) -> &'static str { + AIPLATFORM_SERVICE_KEY + } +} + +#[cfg_attr(feature = "test-utils", automock)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +pub trait AiPlatformApi: Send + Sync + Debug { + /// Submits a supervised tuning job and returns the created [`TuningJob`], + /// whose `name` (`projects/{p}/locations/{l}/tuningJobs/{id}`) is the handle + /// to poll. + async fn create_tuning_job(&self, request: CreateTuningJobRequest) -> Result; + + /// Fetches the current state of a tuning job by its full resource name + /// (`projects/{p}/locations/{l}/tuningJobs/{id}`) or bare id. + async fn get_tuning_job(&self, name: String) -> Result; +} + +/// Vertex AI tuning client. +#[derive(Debug)] +pub struct AiPlatformClient { + base: GcpClientBase, + project_id: String, + location: String, +} + +impl AiPlatformClient { + /// Builds a client pinned to the config's region. Installs the regional + /// `aiplatform` base-URL override (unless the caller already set one, e.g. + /// a test endpoint) so all requests hit `{location}-aiplatform...`. + pub fn new(client: Client, config: GcpClientConfig) -> Self { + let project_id = config.project_id.clone(); + let location = config.region.clone(); + + // Only synthesize the regional host when no override is present; this + // lets tests point the client at a mock endpoint via service overrides. + let mut config = config; + if config.get_service_endpoint_option(AIPLATFORM_SERVICE_KEY).is_none() { + let regional = format!("https://{location}-aiplatform.googleapis.com/v1"); + let mut overrides = config.service_overrides.unwrap_or(ServiceOverrides { + endpoints: std::collections::HashMap::new(), + }); + overrides + .endpoints + .insert(AIPLATFORM_SERVICE_KEY.to_string(), regional); + config.service_overrides = Some(overrides); + } + + Self { + base: GcpClientBase::new(client, config, Box::new(AiPlatformServiceConfig)), + project_id, + location, + } + } + + /// The collection path `projects/{p}/locations/{l}/tuningJobs`. + fn tuning_jobs_path(&self) -> String { + format!( + "projects/{}/locations/{}/tuningJobs", + self.project_id, self.location + ) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl AiPlatformApi for AiPlatformClient { + async fn create_tuning_job(&self, request: CreateTuningJobRequest) -> Result { + let path = self.tuning_jobs_path(); + self.base + .execute_request( + Method::POST, + &path, + None, + Some(request.clone()), + &request.base_model, + ) + .await + } + + async fn get_tuning_job(&self, name: String) -> Result { + // Vertex returns the full resource name; accept either that or a bare id + // and build the region-scoped collection path either way. + let path = if name.contains("/tuningJobs/") { + name.clone() + } else { + format!("{}/{}", self.tuning_jobs_path(), name) + }; + + self.base + .execute_request(Method::GET, &path, None, Option::<()>::None, &name) + .await + } +} + +// --- Data Structures --- + +/// Request body for `tuningJobs.create`. +/// +/// Only the supervised-tuning surface is modelled: `baseModel`, the training +/// dataset URI (a `gs://` path to JSONL), and an optional display name. See +/// . +#[derive(Debug, Serialize, Deserialize, Clone, Builder)] +#[serde(rename_all = "camelCase")] +pub struct CreateTuningJobRequest { + /// Provider-native base model to tune (e.g. a Gemini model id). + pub base_model: String, + + /// Supervised tuning parameters, including the training dataset URI. + pub supervised_tuning_spec: SupervisedTuningSpec, + + /// Optional human-readable name for the resulting tuned model. + #[serde(skip_serializing_if = "Option::is_none")] + pub tuned_model_display_name: Option, +} + +/// Supervised tuning parameters. +#[derive(Debug, Serialize, Deserialize, Clone, Builder)] +#[serde(rename_all = "camelCase")] +pub struct SupervisedTuningSpec { + /// `gs://` URI of the JSONL training dataset in the customer bucket. + pub training_dataset_uri: String, + + /// Optional `gs://` URI of a JSONL validation dataset. + #[serde(skip_serializing_if = "Option::is_none")] + pub validation_dataset_uri: Option, + + /// Optional supervised-tuning hyperparameters. + #[serde(skip_serializing_if = "Option::is_none")] + pub hyper_parameters: Option, +} + +/// Supervised tuning hyperparameters (all optional; Vertex picks defaults). +#[derive(Debug, Serialize, Deserialize, Clone, Default, Builder)] +#[serde(rename_all = "camelCase")] +pub struct SupervisedHyperParameters { + /// Number of complete passes over the training dataset. + #[serde(skip_serializing_if = "Option::is_none")] + pub epoch_count: Option, + + /// Multiplier applied to the recommended learning rate. + #[serde(skip_serializing_if = "Option::is_none")] + pub learning_rate_multiplier: Option, +} + +/// A Vertex AI tuning job. +/// +/// Mirrors the fields the controller needs from +/// . +#[derive(Debug, Serialize, Deserialize, Clone, Default, Builder)] +#[serde(rename_all = "camelCase")] +pub struct TuningJob { + /// Server-assigned resource name: + /// `projects/{p}/locations/{l}/tuningJobs/{id}`. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Current lifecycle state of the job. + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + + /// The tuned model produced once the job reaches `JOB_STATE_SUCCEEDED`. + #[serde(skip_serializing_if = "Option::is_none")] + pub tuned_model: Option, + + /// Populated when the job fails; carries the gRPC-style status. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// Reference to the artifacts a completed tuning job produced. +#[derive(Debug, Serialize, Deserialize, Clone, Default, Builder)] +#[serde(rename_all = "camelCase")] +pub struct TunedModelRef { + /// Resource name of the tuned Model: + /// `projects/{p}/locations/{l}/models/{model}@{version}`. + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + + /// Resource name of the Endpoint serving the tuned model: + /// `projects/{p}/locations/{l}/endpoints/{endpoint}`. This is the id the + /// Vertex OpenAI-compat chat path routes to. + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint: Option, +} + +/// Lifecycle state of a Vertex AI job (subset of `google.cloud.aiplatform.v1.JobState`). +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum JobState { + /// The job state is unspecified. + JobStateUnspecified, + /// The job has been created and is awaiting resources. + JobStateQueued, + /// The job is pending; not yet running. + JobStatePending, + /// The job is currently running. + JobStateRunning, + /// The job completed successfully. + JobStateSucceeded, + /// The job failed. + JobStateFailed, + /// The job is being cancelled. + JobStateCancelling, + /// The job was cancelled. + JobStateCancelled, + /// The job was paused. + JobStatePaused, + /// The job expired. + JobStateExpired, + /// The job is being updated. + JobStateUpdating, + /// The job partially failed (some outputs missing). + JobStatePartiallySucceeded, +} + +impl JobState { + /// Whether the job is still making progress and should be polled again. + pub fn is_in_progress(self) -> bool { + matches!( + self, + JobState::JobStateUnspecified + | JobState::JobStateQueued + | JobState::JobStatePending + | JobState::JobStateRunning + | JobState::JobStateCancelling + | JobState::JobStatePaused + | JobState::JobStateUpdating + ) + } + + /// Whether the job reached a terminal *failure* state (never produces a + /// usable tuned model). + pub fn is_terminal_failure(self) -> bool { + matches!( + self, + JobState::JobStateFailed + | JobState::JobStateCancelled + | JobState::JobStateExpired + | JobState::JobStatePartiallySucceeded + ) + } +} + +impl TunedModelRef { + /// The id the Vertex OpenAI-compat chat endpoint accepts for a tuned model: + /// the serving `endpoint` resource name, falling back to the `model` + /// resource name if the endpoint is absent. + pub fn upstream_id(&self) -> Option<&str> { + self.endpoint + .as_deref() + .or(self.model.as_deref()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_request_serializes_supervised_shape() { + let request = CreateTuningJobRequest::builder() + .base_model("gemini-2.0-flash-001".to_string()) + .supervised_tuning_spec( + SupervisedTuningSpec::builder() + .training_dataset_uri("gs://my-bucket/training.jsonl".to_string()) + .build(), + ) + .tuned_model_display_name("my-ai-tuned".to_string()) + .build(); + + let json = serde_json::to_value(&request).expect("request should serialize"); + + assert_eq!(json["baseModel"], "gemini-2.0-flash-001"); + assert_eq!( + json["supervisedTuningSpec"]["trainingDatasetUri"], + "gs://my-bucket/training.jsonl" + ); + assert_eq!(json["tunedModelDisplayName"], "my-ai-tuned"); + // Optional fields must be omitted, not sent as null (Vertex rejects nulls). + assert!( + json["supervisedTuningSpec"].get("validationDatasetUri").is_none(), + "unset validationDatasetUri must be omitted, got {json:?}" + ); + assert!( + json["supervisedTuningSpec"].get("hyperParameters").is_none(), + "unset hyperParameters must be omitted, got {json:?}" + ); + } + + #[test] + fn tuning_job_deserializes_succeeded_with_endpoint() { + let body = r#"{ + "name": "projects/p/locations/us-central1/tuningJobs/123", + "state": "JOB_STATE_SUCCEEDED", + "tunedModel": { + "model": "projects/p/locations/us-central1/models/456@1", + "endpoint": "projects/p/locations/us-central1/endpoints/789" + } + }"#; + + let job: TuningJob = serde_json::from_str(body).expect("job should deserialize"); + + assert_eq!(job.state, Some(JobState::JobStateSucceeded)); + assert!(job.state.unwrap().is_terminal_failure() == false); + let tuned = job.tuned_model.expect("tuned model present on success"); + // The OpenAI-compat chat path routes to the serving endpoint. + assert_eq!( + tuned.upstream_id(), + Some("projects/p/locations/us-central1/endpoints/789") + ); + } + + #[test] + fn tuning_job_upstream_falls_back_to_model_without_endpoint() { + let tuned = TunedModelRef::builder() + .model("projects/p/locations/us-central1/models/456@1".to_string()) + .build(); + assert_eq!( + tuned.upstream_id(), + Some("projects/p/locations/us-central1/models/456@1") + ); + } + + #[test] + fn tuning_job_deserializes_failed_with_error() { + let body = r#"{ + "name": "projects/p/locations/us-central1/tuningJobs/123", + "state": "JOB_STATE_FAILED", + "error": { "code": 9, "message": "training data invalid" } + }"#; + + let job: TuningJob = serde_json::from_str(body).expect("job should deserialize"); + + let state = job.state.expect("state present"); + assert!(state.is_terminal_failure(), "FAILED must be terminal failure"); + assert!(!state.is_in_progress()); + assert_eq!(job.error.expect("error present").message, "training data invalid"); + } + + #[test] + fn in_progress_states_are_polled_not_terminal() { + for state in [ + JobState::JobStatePending, + JobState::JobStateRunning, + JobState::JobStateQueued, + ] { + assert!(state.is_in_progress(), "{state:?} should be in-progress"); + assert!(!state.is_terminal_failure()); + } + } +} diff --git a/crates/alien-gcp-clients/src/gcp/mod.rs b/crates/alien-gcp-clients/src/gcp/mod.rs index 096e65978..f9cc69d03 100644 --- a/crates/alien-gcp-clients/src/gcp/mod.rs +++ b/crates/alien-gcp-clients/src/gcp/mod.rs @@ -1,3 +1,4 @@ +pub mod aiplatform; pub mod api_client; pub mod artifactregistry; pub mod cloud_sql; diff --git a/crates/alien-gcp-clients/src/lib.rs b/crates/alien-gcp-clients/src/lib.rs index 51e1eccbe..7b43324be 100644 --- a/crates/alien-gcp-clients/src/lib.rs +++ b/crates/alien-gcp-clients/src/lib.rs @@ -12,6 +12,7 @@ pub mod platform { } // Re-export all client APIs +pub use gcp::aiplatform::{AiPlatformApi, AiPlatformClient}; pub use gcp::artifactregistry::{ArtifactRegistryApi, ArtifactRegistryClient}; pub use gcp::cloud_sql::{CloudSqlApi, CloudSqlClient}; pub use gcp::cloudasset::{CloudAssetApi, CloudAssetClient}; diff --git a/crates/alien-infra/src/ai/aws.rs b/crates/alien-infra/src/ai/aws.rs new file mode 100644 index 000000000..087bcf687 --- /dev/null +++ b/crates/alien-infra/src/ai/aws.rs @@ -0,0 +1,425 @@ +use std::time::Duration; + +use tracing::info; + +use crate::core::{ResourceControllerContext, ResourcePermissionsHelper}; +use crate::error::{ErrorData, Result}; +use alien_core::{ + bindings::{AiBinding, FinetuneCapability}, + Ai, AiHeartbeatData, AiHeartbeatStatus, AiOutputs, AwsBedrockAiHeartbeatData, HeartbeatBackend, + Platform, ResourceHeartbeat, ResourceHeartbeatData, ResourceOutputs, ResourceRef, + ResourceStatus, Storage, +}; +use alien_error::{AlienError, Context, IntoAlienError}; +use alien_macros::controller; +use chrono::Utc; + +#[controller] +pub struct AwsAiController { + /// AWS region where Bedrock is accessed. None until create_start runs. + pub(crate) region: Option, + /// The fine-tuning capability this resource carries on its binding, resolved + /// during the create flow from the training-storage dependency and the + /// deterministic finetune role name. `get_binding_params` is a pure function of + /// controller state (no `ctx`), so it is captured here rather than resolved + /// live. None for a pure inference gateway. + pub(crate) finetune: Option, +} + +#[controller] +impl AwsAiController { + // ─────────────── CREATE FLOW ────────────────────────────── + + #[flow_entry(Create)] + #[handler( + state = CreateStart, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn create_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + let aws_config = ctx.get_aws_config()?; + self.region = Some(aws_config.region.clone()); + + info!(id=%config.id, region=%aws_config.region, "AWS AI (Bedrock) controller: no resource to create, applying permissions"); + + Ok(HandlerAction::Continue { + state: ApplyingResourcePermissions, + suggested_delay: None, + }) + } + + #[handler( + state = ApplyingResourcePermissions, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn applying_resource_permissions( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let config = ctx.desired_resource_config::()?; + + info!(ai=%config.id, "Applying resource-scoped permissions for Bedrock AI gateway"); + + // Bedrock invoke grants use foundation-model/* ARNs (not per-resource ARNs). + // config.id is passed as resource_name; the ai/invoke permission set binding + // uses `arn:aws:bedrock:*::foundation-model/*` which is region/account-wide. + ResourcePermissionsHelper::apply_aws_resource_scoped_permissions( + ctx, &config.id, &config.id, "ai", + ) + .await?; + + info!(ai=%config.id, "Successfully applied resource-scoped permissions"); + + // A finetune-enabled resource carries a `FinetuneCapability` on its binding so + // the gateway can submit and rediscover a runtime tuning job. Resolve it here + // (where `ctx` is available) and stash it on the controller; `get_binding_params` + // is a pure function of state and reads it back. The tuning job itself is NOT + // submitted here — it is triggered at runtime via the gateway API — so the + // resource is Ready as soon as permissions are applied, whether or not it + // declares a `finetune` spec. + self.finetune = resolve_finetune_capability(ctx).await?; + + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── READY STATE ──────────────────────────────── + // Loops as a heartbeat tick; Bedrock has no per-stack resource to poll. + + #[handler( + state = Ready, + on_failure = RefreshFailed, + status = ResourceStatus::Running, + )] + async fn ready(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + let region = self.region.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + resource_id: Some(config.id.clone()), + message: "Region not set in state".to_string(), + }) + })?; + info!(id=%config.id, "AWS AI heartbeat tick"); + ctx.emit_heartbeat(ResourceHeartbeat { + deployment_id: None, + resource_id: config.id.clone(), + resource_type: Ai::RESOURCE_TYPE, + controller_platform: Platform::Aws, + backend: HeartbeatBackend::Aws, + observed_at: Utc::now(), + data: ResourceHeartbeatData::Ai(AiHeartbeatData::AwsBedrock( + AwsBedrockAiHeartbeatData { + status: AiHeartbeatStatus::default(), + region: region.clone(), + }, + )), + raw: vec![], + }); + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: Some(Duration::from_secs(30)), + }) + } + + // ─────────────── UPDATE FLOW ────────────────────────────── + // Ai has no mutable inference fields, and finetune base/training are immutable + // (enforced in `Ai::validate_update`), so update is a no-op that also recovers + // RefreshFailed. The resolved finetune capability persists in state across updates. + + #[flow_entry(Update, from = [Ready, RefreshFailed])] + #[handler( + state = UpdateStart, + on_failure = UpdateFailed, + status = ResourceStatus::Updating, + )] + async fn update_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + info!(id=%config.id, "AWS AI update (no-op -- no mutable fields)"); + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── DELETE FLOW ────────────────────────────── + // AWS AI creates no cloud resource; deletion is always a no-op. + + #[flow_entry(Delete)] + #[handler( + state = DeleteStart, + on_failure = DeleteFailed, + status = ResourceStatus::Deleting, + )] + async fn delete_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + info!(id=%config.id, "AWS AI delete (no-op -- Bedrock has no per-stack resource to remove)"); + Ok(HandlerAction::Continue { + state: Deleted, + suggested_delay: None, + }) + } + + // ─────────────── TERMINALS ──────────────────────────────── + + terminal_state!( + state = CreateFailed, + status = ResourceStatus::ProvisionFailed + ); + + terminal_state!(state = UpdateFailed, status = ResourceStatus::UpdateFailed); + + terminal_state!(state = DeleteFailed, status = ResourceStatus::DeleteFailed); + + terminal_state!( + state = RefreshFailed, + status = ResourceStatus::RefreshFailed + ); + + terminal_state!(state = Deleted, status = ResourceStatus::Deleted); + + fn build_outputs(&self) -> Option { + let region = self.region.as_ref()?; + Some(ResourceOutputs::new(AiOutputs { + provider: "bedrock".into(), + endpoint: Some(format!("https://bedrock-mantle.{}.api.aws/v1", region)), + account: None, + })) + } + + fn get_binding_params(&self) -> Result> { + let region = match &self.region { + Some(r) => r, + None => return Ok(None), + }; + + let mut binding = AiBinding::bedrock(region); + + // A finetune-enabled resource carries a `FinetuneCapability` so the gateway + // can submit and rediscover a runtime tuning job. No tuned model is attached + // here — the gateway rediscovers it by convention once a runtime job + // succeeds. A pure inference gateway emits the plain binding unchanged. + if let Some(capability) = &self.finetune { + binding = binding.with_finetune(capability.clone()); + } + + Ok(Some(serde_json::to_value(binding).into_alien_error().context( + ErrorData::ResourceStateSerializationFailed { + resource_id: "binding".to_string(), + message: "Failed to serialize AI binding parameters".to_string(), + }, + )?)) + } +} + +/// Resolve the fine-tuning capability from the declared `finetune` spec, the +/// training-storage dependency's real bucket name, and the deterministic +/// Bedrock-trusted finetune role. Returns `None` for a pure inference gateway. +async fn resolve_finetune_capability( + ctx: &ResourceControllerContext<'_>, +) -> Result> { + let config = ctx.desired_resource_config::()?; + let Some(spec) = config.finetune.as_ref() else { + return Ok(None); + }; + let aws_config = ctx.get_aws_config()?; + + // Resolve the training bucket from the dependency's real state rather than + // re-deriving the name. The Ai resource declares its training-data Storage as a + // dependency (see `Ai::get_dependencies`), and the AWS storage controller stores + // the actual (prefixed) bucket name in `bucket_name`. + let training_ref = ResourceRef::new(Storage::RESOURCE_TYPE, spec.training_data.clone()); + let storage_state = + ctx.require_dependency::(&training_ref)?; + let training_bucket = storage_state.bucket_name.clone().ok_or_else(|| { + AlienError::new(ErrorData::DependencyNotReady { + resource_id: config.id.clone(), + dependency_id: spec.training_data.clone(), + }) + })?; + + Ok(Some(FinetuneCapability { + base_model: spec.base_model.clone(), + training_bucket, + training_key: spec.training_key.clone(), + served_model_id: spec.served_model_id_or_default(&config.id), + job_name: format!("{}-{}", ctx.resource_prefix, config.id), + // The dedicated Bedrock-trusted finetune role emitted by the AWS emitter + // (`{prefix}-{id}-finetune`). The gateway passes this ARN as the tuning job's + // `roleArn` so Bedrock can read training data and write output. + role_arn: format!( + "arn:aws:iam::{}:role/{}-{}-finetune", + aws_config.account_id, ctx.resource_prefix, config.id + ), + })) +} + +#[cfg(all(test, feature = "test-utils"))] +mod tests { + use super::*; + use crate::core::controller_test::SingleControllerExecutor; + use crate::core::ResourceController; + use crate::storage::AwsStorageController; + use crate::MockPlatformServiceProvider; + use alien_core::bindings::AiBinding; + use alien_core::{Ai, FinetuneMethod, FinetuneSpec, Platform, ResourceStatus, Storage}; + use std::sync::Arc; + + // The training-data storage the finetune resource depends on. Its AWS storage + // controller mock is wired with a real bucket name so the AI controller can + // read it via `require_dependency`. + const TRAINING_STORAGE_ID: &str = "training-set"; + + fn training_storage() -> Storage { + Storage::new(TRAINING_STORAGE_ID.to_string()).build() + } + + fn tuned_ai() -> Ai { + Ai::new("my-ai".to_string()) + .finetune(FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: TRAINING_STORAGE_ID.to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }) + .build() + } + + fn untuned_ai() -> Ai { + Ai::new("my-ai".to_string()).build() + } + + /// A provider that must never touch Bedrock: the runtime model submits tuning + /// jobs from the gateway, never from the controller, so the controller must not + /// construct a Bedrock client at deploy time. + fn provider_without_bedrock() -> Arc { + let mut provider = MockPlatformServiceProvider::new(); + provider.expect_get_aws_bedrock_client().never(); + Arc::new(provider) + } + + /// The real (prefixed) bucket name the storage dependency mock resolves to. + /// `AwsStorageController::mock_ready` prefixes with "test-stack". + fn training_bucket_name() -> String { + format!("test-stack-{}", TRAINING_STORAGE_ID) + } + + /// Read the controller's current binding via the ResourceController trait. + fn current_binding(executor: &SingleControllerExecutor) -> AiBinding { + let controller = executor + .internal_state::() + .expect("controller is AwsAiController"); + let value = controller + .get_binding_params() + .expect("binding params resolve") + .expect("binding present once region is set"); + serde_json::from_value(value).expect("binding deserializes") + } + + /// Step the executor until it reaches Running (Ready is a heartbeat loop, not a + /// terminal state) or the bound is exhausted. + async fn drive_to_running(executor: &mut SingleControllerExecutor, steps: usize) { + for _ in 0..steps { + if executor.status() == ResourceStatus::Running { + return; + } + executor.step().await.expect("step should not error"); + } + } + + #[tokio::test] + async fn test_finetune_reaches_ready_immediately_with_capability_and_no_job() { + // The runtime model: a finetune resource reaches Ready as soon as + // permissions are applied — NO Bedrock job is submitted at deploy time — and + // its binding carries a FinetuneCapability with the resolved training bucket + // and the dedicated `-finetune` role ARN. + let provider = provider_without_bedrock(); + + let mut executor = SingleControllerExecutor::builder() + .resource(tuned_ai()) + .controller(AwsAiController::default()) + .platform(Platform::Aws) + .service_provider(provider) + .with_dependency( + training_storage(), + AwsStorageController::mock_ready(TRAINING_STORAGE_ID), + ) + .build() + .await + .expect("executor builds"); + + drive_to_running(&mut executor, 6).await; + assert_eq!( + executor.status(), + ResourceStatus::Running, + "a finetune resource must reach Ready immediately (no deploy-time job)" + ); + + let binding = current_binding(&executor); + assert!( + binding.tuned_model().is_none(), + "no tuned model is attached at deploy time; the gateway rediscovers it" + ); + + let capability = binding + .finetune() + .expect("finetune resource must carry a FinetuneCapability"); + assert_eq!(capability.base_model, "amazon.nova-lite-v1:0"); + assert_eq!( + capability.training_bucket, + training_bucket_name(), + "training bucket must come from the storage dependency's real state" + ); + assert_eq!(capability.training_key, "training.jsonl"); + assert_eq!( + capability.served_model_id, "my-ai-tuned", + "served id must default to -tuned" + ); + assert_eq!( + capability.job_name, "test-my-ai", + "job name must be the deterministic {{prefix}}-{{id}}" + ); + assert_eq!( + capability.role_arn, "arn:aws:iam::123456789012:role/test-my-ai-finetune", + "role ARN must name the dedicated Bedrock-trusted `-finetune` role" + ); + } + + #[tokio::test] + async fn test_no_finetune_reaches_ready_with_plain_binding() { + // Regression: an inference-only resource never touches Bedrock tuning and + // emits a plain binding with neither a tuned model nor a capability. + let provider = provider_without_bedrock(); + + let mut executor = SingleControllerExecutor::builder() + .resource(untuned_ai()) + .controller(AwsAiController::default()) + .platform(Platform::Aws) + .service_provider(provider) + .with_test_dependencies() + .build() + .await + .expect("executor builds"); + + drive_to_running(&mut executor, 6).await; + assert_eq!( + executor.status(), + ResourceStatus::Running, + "inference-only resource must reach Ready" + ); + + let binding = current_binding(&executor); + assert!( + binding.tuned_model().is_none(), + "inference-only binding must omit tuned_model" + ); + assert!( + binding.finetune().is_none(), + "inference-only binding must omit the finetune capability" + ); + } +} diff --git a/crates/alien-infra/src/ai/aws_import.rs b/crates/alien-infra/src/ai/aws_import.rs new file mode 100644 index 000000000..484fb8004 --- /dev/null +++ b/crates/alien-infra/src/ai/aws_import.rs @@ -0,0 +1,32 @@ +//! Importer for AWS AI (Bedrock). +//! +//! AWS AI creates no cloud resource; the importer carries the region from the +//! import payload directly into the controller state. + +use alien_core::{ + import::{data::AwsAiImportData, ImportContext}, + Result, StackResourceState, +}; + +use crate::import::ResourceImporter; +use crate::import_helpers::make_imported_state; +use crate::ai::aws::AwsAiState; +use crate::ai::AwsAiController; + +/// AWS Bedrock AI importer. +#[derive(Debug, Default)] +pub struct AwsAiImporter; + +impl ResourceImporter for AwsAiImporter { + type ImportData = AwsAiImportData; + + fn import(&self, data: AwsAiImportData, ctx: &ImportContext<'_>) -> Result { + let controller = AwsAiController { + state: AwsAiState::Ready, + region: Some(data.region), + finetune: None, + _internal_stay_count: None, + }; + make_imported_state(controller, ctx) + } +} diff --git a/crates/alien-infra/src/ai/azure.rs b/crates/alien-infra/src/ai/azure.rs new file mode 100644 index 000000000..a6fafd592 --- /dev/null +++ b/crates/alien-infra/src/ai/azure.rs @@ -0,0 +1,1231 @@ +use tracing::info; + +use crate::azure_utils::get_resource_group_name; +use crate::core::{ResourceControllerContext, ResourcePermissionsHelper}; +use crate::error::{ErrorData, Result}; +use crate::storage::AzureStorageController; +use alien_azure_clients::azure::cognitive_services::{ + CognitiveServicesAccountCreateParameters, CognitiveServicesAccountCreateProperties, + CognitiveServicesDeploymentCreateParameters, CognitiveServicesDeploymentCreateProperties, + CognitiveServicesDeploymentModel, CognitiveServicesDeploymentSku, CognitiveServicesSku, +}; +use alien_azure_clients::long_running_operation::OperationResult; +use alien_client_core::ErrorData as CloudClientErrorData; +use alien_core::{ + bindings::{AiBinding, FinetuneCapability}, + Ai, AiHeartbeatData, AiHeartbeatStatus, AiOutputs, AzureFoundryAiHeartbeatData, + HeartbeatBackend, Platform, ResourceHeartbeat, ResourceHeartbeatData, ResourceOutputs, + ResourceRef, ResourceStatus, Storage, +}; +use alien_error::{AlienError, Context, ContextError, IntoAlienError}; +use alien_macros::controller; +use chrono::Utc; + +/// Provisioned throughput (in GlobalStandard units) for each predefined model +/// deployment. A conservative default; per-region quota tuning is a deploy-time +/// concern verified against the target subscription. +const DEFAULT_DEPLOYMENT_CAPACITY: i32 = 1; + +/// Derives a globally-unique AIServices account name from the stack prefix and +/// resource id. Azure account names must be 2-64 chars, alphanumeric + hyphens, +/// start with a letter, and end with a letter or digit. +fn make_account_name(prefix: &str, id: &str) -> String { + let raw = format!("{}-{}", prefix, id); + // Keep only alphanumeric chars and hyphens; collapse leading/trailing hyphens. + let cleaned: String = raw + .chars() + .map(|c| if c.is_alphanumeric() || c == '-' { c } else { '-' }) + .collect(); + let trimmed = cleaned.trim_matches('-').to_string(); + if trimmed.len() > 64 { + trimmed[..64].trim_end_matches('-').to_string() + } else { + trimmed + } +} + +#[controller] +pub struct AzureAiController { + /// The name of the Azure AIServices account. + pub(crate) account_name: Option, + /// The endpoint URL returned by the AIServices account once provisioned. + pub(crate) endpoint: Option, + /// The Azure resource group containing the account. + pub(crate) resource_group: Option, + /// The Azure region where the account is created. + pub(crate) location: Option, + /// The fine-tuning capability this gateway advertises, resolved during the + /// create flow when the resource declares a `finetune` spec. The gateway + /// submits and rediscovers the Foundry fine-tuning job at runtime from this + /// capability; the controller never starts a job itself. Captured into state + /// (rather than rebuilt in `get_binding_params`, which has no `ctx`) so the + /// binding can carry it. `None` for a pure-inference gateway. + #[serde(default)] + pub(crate) finetune: Option, +} + +#[controller] +impl AzureAiController { + // ─────────────── CREATE FLOW ────────────────────────────── + + #[flow_entry(Create)] + #[handler( + state = CreateStart, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn create_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let azure_config = ctx.get_azure_config()?; + let config = ctx.desired_resource_config::()?; + + let account_name = make_account_name(&ctx.resource_prefix, &config.id); + let resource_group_name = get_resource_group_name(&ctx.state)?; + let location = azure_config.region.clone().ok_or_else(|| { + AlienError::new(ErrorData::ClientConfigInvalid { + platform: Platform::Azure, + message: "Azure region is required but not specified in configuration".to_string(), + }) + })?; + + info!( + id = %config.id, + account_name = %account_name, + resource_group = %resource_group_name, + location = %location, + "Creating Azure AIServices account" + ); + + let cognitive_client = ctx + .service_provider + .get_azure_cognitive_services_client(azure_config)?; + + let parameters = CognitiveServicesAccountCreateParameters { + location: location.clone(), + kind: "AIServices".to_string(), + sku: CognitiveServicesSku { + name: "S0".to_string(), + }, + properties: CognitiveServicesAccountCreateProperties { + custom_sub_domain_name: account_name.clone(), + }, + }; + + let operation_result = cognitive_client + .create_account(&resource_group_name, &account_name, ¶meters) + .await + .context(ErrorData::CloudPlatformError { + message: format!("Failed to create Azure AIServices account '{}'", account_name), + resource_id: Some(config.id.clone()), + })?; + + // Store these before matching so both branches can reference them. + self.account_name = Some(account_name.clone()); + self.resource_group = Some(resource_group_name.clone()); + self.location = Some(location); + + match operation_result { + OperationResult::Completed(account) => { + self.endpoint = account + .properties + .and_then(|p| p.endpoint); + + info!(account_name = %account_name, "Azure AIServices account created (synchronous)"); + + Ok(HandlerAction::Continue { + state: WaitingForAccountCreation, + suggested_delay: None, + }) + } + OperationResult::LongRunning(_) => { + info!(account_name = %account_name, "Azure AIServices account creation is in progress"); + + Ok(HandlerAction::Continue { + state: WaitingForAccountCreation, + suggested_delay: Some(std::time::Duration::from_secs(10)), + }) + } + } + } + + #[handler( + state = WaitingForAccountCreation, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn waiting_for_account_creation( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let azure_config = ctx.get_azure_config()?; + let config = ctx.desired_resource_config::()?; + + let account_name = self.account_name.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Account name not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + let resource_group_name = self.resource_group.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Resource group not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + + info!(account_name = %account_name, "Polling Azure AIServices account provisioning state"); + + let cognitive_client = ctx + .service_provider + .get_azure_cognitive_services_client(azure_config)?; + + match cognitive_client + .get_account(resource_group_name, account_name) + .await + { + Ok(account) => { + let provisioning_state = account + .properties + .as_ref() + .and_then(|p| p.provisioning_state.as_deref()) + .unwrap_or(""); + + if provisioning_state.eq_ignore_ascii_case("Succeeded") { + self.endpoint = account.properties.and_then(|p| p.endpoint); + + info!( + account_name = %account_name, + endpoint = ?self.endpoint, + "Azure AIServices account provisioned successfully" + ); + + Ok(HandlerAction::Continue { + state: ApplyingResourcePermissions, + suggested_delay: None, + }) + } else { + info!( + account_name = %account_name, + provisioning_state = %provisioning_state, + "Azure AIServices account not yet ready, retrying" + ); + + Ok(HandlerAction::Continue { + state: WaitingForAccountCreation, + suggested_delay: Some(std::time::Duration::from_secs(10)), + }) + } + } + Err(e) + if matches!( + &e.error, + Some(CloudClientErrorData::RemoteResourceNotFound { .. }) + ) => + { + info!(account_name = %account_name, "Azure AIServices account not yet visible, retrying"); + Ok(HandlerAction::Continue { + state: WaitingForAccountCreation, + suggested_delay: Some(std::time::Duration::from_secs(10)), + }) + } + Err(e) => Err(e.context(ErrorData::CloudPlatformError { + message: format!( + "Failed to poll Azure AIServices account '{}' provisioning state", + account_name + ), + resource_id: Some(config.id.clone()), + })), + } + } + + #[handler( + state = ApplyingResourcePermissions, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn applying_resource_permissions( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let config = ctx.desired_resource_config::()?; + + info!(id = %config.id, "Applying Cognitive Services OpenAI User role on AIServices account"); + + let account_name = self.account_name.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Account name not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + let resource_group_name = self.resource_group.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Resource group not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + + use alien_azure_clients::authorization::Scope; + + let resource_scope = Scope::Resource { + resource_group_name: resource_group_name.clone(), + resource_provider: "Microsoft.CognitiveServices".to_string(), + parent_resource_path: None, + resource_type: "accounts".to_string(), + resource_name: account_name.clone(), + }; + + ResourcePermissionsHelper::apply_azure_resource_scoped_permissions( + ctx, + &config.id, + account_name, + resource_scope, + "AI", + "ai", + ) + .await?; + + info!(id = %config.id, "Successfully applied Cognitive Services OpenAI User role"); + + Ok(HandlerAction::Continue { + state: DeployingModels, + suggested_delay: None, + }) + } + + // ─────────────── MODEL DEPLOYMENT ─────────────────────────── + + #[handler( + state = DeployingModels, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn deploying_models( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let azure_config = ctx.get_azure_config()?; + let config = ctx.desired_resource_config::()?; + + let account_name = self.account_name.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Account name not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + let resource_group_name = self.resource_group.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Resource group not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + + let cognitive_client = ctx + .service_provider + .get_azure_cognitive_services_client(azure_config)?; + + // PUT is idempotent, so re-entering this state (e.g. after a retry) re-issues + // the same deployments harmlessly. + for (deployment_name, model_name, model_version) in alien_core::ai_catalog::azure_deployments() + { + info!( + account_name = %account_name, + deployment = %deployment_name, + "Creating Azure model deployment" + ); + + let parameters = CognitiveServicesDeploymentCreateParameters { + sku: CognitiveServicesDeploymentSku { + name: "GlobalStandard".to_string(), + capacity: DEFAULT_DEPLOYMENT_CAPACITY, + }, + properties: CognitiveServicesDeploymentCreateProperties { + model: CognitiveServicesDeploymentModel { + format: "OpenAI".to_string(), + name: model_name.to_string(), + version: model_version.to_string(), + }, + }, + }; + + cognitive_client + .create_deployment( + resource_group_name, + account_name, + deployment_name, + ¶meters, + ) + .await + .context(ErrorData::CloudPlatformError { + message: format!( + "Failed to create Azure model deployment '{}'", + deployment_name + ), + resource_id: Some(config.id.clone()), + })?; + } + + Ok(HandlerAction::Continue { + state: WaitingForDeployments, + suggested_delay: Some(std::time::Duration::from_secs(10)), + }) + } + + #[handler( + state = WaitingForDeployments, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn waiting_for_deployments( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let azure_config = ctx.get_azure_config()?; + let config = ctx.desired_resource_config::()?; + + let account_name = self.account_name.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Account name not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + let resource_group_name = self.resource_group.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Resource group not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + + let cognitive_client = ctx + .service_provider + .get_azure_cognitive_services_client(azure_config)?; + + for (deployment_name, _, _) in alien_core::ai_catalog::azure_deployments() { + match cognitive_client + .get_deployment(resource_group_name, account_name, deployment_name) + .await + { + Ok(deployment) => { + let provisioning_state = deployment + .properties + .as_ref() + .and_then(|p| p.provisioning_state.as_deref()) + .unwrap_or(""); + + // Deployments routinely reach a terminal failure (in-region + // capacity/quota, or the model version not offered there). Fail + // fast to CreateFailed rather than polling a dead deployment forever. + if provisioning_state.eq_ignore_ascii_case("Failed") + || provisioning_state.eq_ignore_ascii_case("Canceled") + { + return Err(AlienError::new(ErrorData::CloudPlatformError { + message: format!( + "Azure model deployment '{}' entered terminal state '{}'", + deployment_name, provisioning_state + ), + resource_id: Some(config.id.clone()), + })); + } + + if !provisioning_state.eq_ignore_ascii_case("Succeeded") { + info!( + account_name = %account_name, + deployment = %deployment_name, + provisioning_state = %provisioning_state, + "Model deployment not yet ready, retrying" + ); + return Ok(HandlerAction::Continue { + state: WaitingForDeployments, + suggested_delay: Some(std::time::Duration::from_secs(10)), + }); + } + } + Err(e) + if matches!( + &e.error, + Some(CloudClientErrorData::RemoteResourceNotFound { .. }) + ) => + { + info!( + account_name = %account_name, + deployment = %deployment_name, + "Model deployment not yet visible, retrying" + ); + return Ok(HandlerAction::Continue { + state: WaitingForDeployments, + suggested_delay: Some(std::time::Duration::from_secs(10)), + }); + } + Err(e) => { + return Err(e.context(ErrorData::CloudPlatformError { + message: format!( + "Failed to poll Azure model deployment '{}'", + deployment_name + ), + resource_id: Some(config.id.clone()), + })) + } + } + } + + info!(account_name = %account_name, "All Azure model deployments provisioned"); + + // Fine-tuning is triggered at runtime through the gateway, not at deploy + // time, so the resource is Ready as soon as the base deployments succeed. + // When the resource declares a `finetune` spec, resolve the capability the + // gateway needs to submit/rediscover a runtime tuning job and carry it on + // the binding. + self.finetune = self.resolve_finetune_capability(ctx, &config)?; + + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── READY STATE ──────────────────────────────── + + #[handler( + state = Ready, + on_failure = RefreshFailed, + status = ResourceStatus::Running, + )] + async fn ready(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + + let account_name = self.account_name.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + resource_id: Some(config.id.clone()), + message: "Account name not set in state".to_string(), + }) + })?; + let _endpoint = self.endpoint.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + resource_id: Some(config.id.clone()), + message: "Endpoint not set in state".to_string(), + }) + })?; + + info!(id = %config.id, "Azure AI heartbeat tick"); + + ctx.emit_heartbeat(ResourceHeartbeat { + deployment_id: None, + resource_id: config.id.clone(), + resource_type: Ai::RESOURCE_TYPE, + controller_platform: Platform::Azure, + backend: HeartbeatBackend::Azure, + observed_at: Utc::now(), + data: ResourceHeartbeatData::Ai(AiHeartbeatData::AzureFoundry( + AzureFoundryAiHeartbeatData { + status: AiHeartbeatStatus::default(), + account_name: account_name.clone(), + endpoint: self.endpoint.clone(), + resource_group: self.resource_group.clone(), + location: self.location.clone(), + }, + )), + raw: vec![], + }); + + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: Some(std::time::Duration::from_secs(30)), + }) + } + + // ─────────────── UPDATE FLOW ────────────────────────────── + + #[flow_entry(Update, from = [Ready, RefreshFailed])] + #[handler( + state = UpdateStart, + on_failure = UpdateFailed, + status = ResourceStatus::Updating, + )] + async fn update_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + info!(id = %config.id, "Azure AI update (no-op -- no mutable fields)"); + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── DELETE FLOW ────────────────────────────── + + #[flow_entry(Delete)] + #[handler( + state = DeleteStart, + on_failure = DeleteFailed, + status = ResourceStatus::Deleting, + )] + async fn delete_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let azure_config = ctx.get_azure_config()?; + let config = ctx.desired_resource_config::()?; + + let account_name = match &self.account_name { + Some(name) => name.clone(), + None => { + info!(id = %config.id, "No Azure AIServices account to delete"); + return Ok(HandlerAction::Continue { + state: Deleted, + suggested_delay: None, + }); + } + }; + let resource_group_name = self.resource_group.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Resource group not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + + info!( + id = %config.id, + account_name = %account_name, + "Deleting Azure AIServices account" + ); + + let cognitive_client = ctx + .service_provider + .get_azure_cognitive_services_client(azure_config)?; + + match cognitive_client + .delete_account(resource_group_name, &account_name) + .await + { + Ok(()) => { + info!(account_name = %account_name, "Azure AIServices account deleted, polling for removal"); + Ok(HandlerAction::Continue { + state: WaitingForAccountDeletion, + suggested_delay: Some(std::time::Duration::from_secs(5)), + }) + } + Err(e) + if matches!( + e.error, + Some(CloudClientErrorData::RemoteResourceNotFound { .. }) + ) => + { + info!(account_name = %account_name, "Azure AIServices account already deleted"); + self.clear_state(); + Ok(HandlerAction::Continue { + state: Deleted, + suggested_delay: None, + }) + } + Err(e) => Err(e.context(ErrorData::CloudPlatformError { + message: format!("Failed to delete Azure AIServices account '{}'", account_name), + resource_id: Some(config.id.clone()), + })), + } + } + + #[handler( + state = WaitingForAccountDeletion, + on_failure = DeleteFailed, + status = ResourceStatus::Deleting, + )] + async fn waiting_for_account_deletion( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let azure_config = ctx.get_azure_config()?; + let config = ctx.desired_resource_config::()?; + + let account_name = self.account_name.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Account name not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + let resource_group_name = self.resource_group.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + message: "Resource group not set in state".to_string(), + resource_id: Some(config.id.clone()), + }) + })?; + + info!(account_name = %account_name, "Polling Azure AIServices account deletion"); + + let cognitive_client = ctx + .service_provider + .get_azure_cognitive_services_client(azure_config)?; + + match cognitive_client + .get_account(resource_group_name, account_name) + .await + { + Err(e) + if matches!( + &e.error, + Some(CloudClientErrorData::RemoteResourceNotFound { .. }) + ) => + { + info!(account_name = %account_name, "Azure AIServices account confirmed deleted"); + self.clear_state(); + Ok(HandlerAction::Continue { + state: Deleted, + suggested_delay: None, + }) + } + Ok(_) => { + info!( + account_name = %account_name, + "Azure AIServices account still exists, retrying deletion poll" + ); + Ok(HandlerAction::Continue { + state: WaitingForAccountDeletion, + suggested_delay: Some(std::time::Duration::from_secs(10)), + }) + } + Err(e) => Err(e.context(ErrorData::CloudPlatformError { + message: format!( + "Failed to poll Azure AIServices account '{}' deletion state", + account_name + ), + resource_id: Some(config.id.clone()), + })), + } + } + + // ─────────────── TERMINALS ──────────────────────────────── + + terminal_state!( + state = CreateFailed, + status = ResourceStatus::ProvisionFailed + ); + + terminal_state!(state = UpdateFailed, status = ResourceStatus::UpdateFailed); + + terminal_state!(state = DeleteFailed, status = ResourceStatus::DeleteFailed); + + terminal_state!( + state = RefreshFailed, + status = ResourceStatus::RefreshFailed + ); + + terminal_state!(state = Deleted, status = ResourceStatus::Deleted); + + fn build_outputs(&self) -> Option { + let endpoint = self.endpoint.clone()?; + let account_name = self.account_name.clone()?; + Some(ResourceOutputs::new(AiOutputs { + provider: "foundry".into(), + endpoint: Some(endpoint), + account: Some(account_name), + })) + } + + fn get_binding_params(&self) -> Result> { + let endpoint = match &self.endpoint { + Some(ep) => ep.clone(), + None => return Ok(None), + }; + let account_name = match &self.account_name { + Some(a) => a.clone(), + None => return Ok(None), + }; + + let mut binding = AiBinding::foundry(endpoint, account_name); + + // Carry the fine-tuning capability when the resource declared one, so the + // gateway can submit/rediscover a runtime tuning job. A pure-inference + // gateway returns the untuned binding unchanged. The controller never + // attaches a `tuned_model` — the gateway rediscovers it by convention. + if let Some(capability) = self.finetune.as_ref() { + binding = binding.with_finetune(capability.clone()); + } + + Ok(Some(serde_json::to_value(binding).into_alien_error().context( + ErrorData::ResourceStateSerializationFailed { + resource_id: "binding".to_string(), + message: "Failed to serialize Azure AI binding parameters".to_string(), + }, + )?)) + } +} + +impl AzureAiController { + fn clear_state(&mut self) { + self.account_name = None; + self.endpoint = None; + self.resource_group = None; + self.location = None; + self.finetune = None; + } + + /// Builds the fine-tuning capability the binding carries when the resource + /// declares a `finetune` spec, or `None` for a pure-inference gateway. + /// + /// Resolves the training data's real Blob container from the storage + /// dependency's controller state (rather than re-deriving the prefixed name), + /// keeping it in lockstep with the storage controller's naming. Foundry + /// submits the runtime tuning job under the gateway's ambient identity, so no + /// role is passed (`role_arn` is empty). + fn resolve_finetune_capability( + &self, + ctx: &ResourceControllerContext<'_>, + config: &Ai, + ) -> Result> { + let Some(spec) = config.finetune.as_ref() else { + return Ok(None); + }; + + let training_ref = ResourceRef::new(Storage::RESOURCE_TYPE, spec.training_data.clone()); + let storage_state = ctx.require_dependency::(&training_ref)?; + let training_bucket = storage_state.container_name.ok_or_else(|| { + AlienError::new(ErrorData::DependencyNotReady { + resource_id: config.id.clone(), + dependency_id: spec.training_data.clone(), + }) + })?; + + Ok(Some(FinetuneCapability { + base_model: spec.base_model.clone(), + training_bucket, + training_key: spec.training_key.clone(), + served_model_id: spec.served_model_id_or_default(&config.id), + job_name: format!("{}-{}", ctx.resource_prefix, config.id), + // Foundry submits under the ambient identity; no passed role. + role_arn: String::new(), + })) + } + + /// Creates a controller in the ready state for testing purposes. + #[cfg(feature = "test-utils")] + pub fn mock_ready(account_name: &str, endpoint: &str) -> Self { + Self { + state: AzureAiState::Ready, + account_name: Some(account_name.to_string()), + endpoint: Some(endpoint.to_string()), + resource_group: Some("mock-rg".to_string()), + location: Some("eastus".to_string()), + finetune: None, + _internal_stay_count: None, + } + } + + /// Creates a controller poised to deploy the predefined model set (account + /// already provisioned), for testing the model-deployment states. + #[cfg(feature = "test-utils")] + pub fn mock_deploying(account_name: &str, endpoint: &str) -> Self { + Self { + state: AzureAiState::DeployingModels, + account_name: Some(account_name.to_string()), + endpoint: Some(endpoint.to_string()), + resource_group: Some("mock-rg".to_string()), + location: Some("eastus".to_string()), + finetune: None, + _internal_stay_count: None, + } + } +} + +#[cfg(all(test, feature = "test-utils"))] +mod tests { + use super::*; + use crate::core::controller_test::SingleControllerExecutor; + use crate::core::ResourceController; + use crate::MockPlatformServiceProvider; + use alien_azure_clients::azure::cognitive_services::{ + CognitiveServicesDeployment, CognitiveServicesDeploymentProperties, + MockCognitiveServicesAccountsApi, + }; + use alien_azure_clients::long_running_operation::OperationResult; + use alien_client_core::ErrorData as CloudClientErrorData; + use crate::storage::AzureStorageController; + use alien_core::bindings::AiBinding; + use alien_core::{ + Ai, AiOutputs, FinetuneMethod, FinetuneSpec, Platform, ResourceStatus, Storage, + }; + use alien_error::AlienError; + use std::sync::Arc; + + fn basic_ai() -> Ai { + Ai::new("my-ai".to_string()).build() + } + + /// The training-data storage id the tuned resource depends on. Matches the + /// Azure test storage account dependency wired by `with_test_dependencies`. + const TRAINING_STORAGE_ID: &str = "training-set"; + + fn tuned_ai() -> Ai { + Ai::new("my-ai".to_string()) + .finetune(FinetuneSpec { + base_model: "gpt-4o-mini".to_string(), + training_data: TRAINING_STORAGE_ID.to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }) + .build() + } + + /// The training-data Storage the tuned resource declares as a dependency + /// (via `Ai::get_dependencies`). Must be wired Ready or the executor won't + /// run the AI controller. + fn training_storage() -> Storage { + Storage::new(TRAINING_STORAGE_ID.to_string()).build() + } + + /// A cognitive-services mock that succeeds all base deployments (create + + /// get both report Succeeded). + fn mock_cognitive_all_succeed() -> MockCognitiveServicesAccountsApi { + let mut mock = MockCognitiveServicesAccountsApi::new(); + mock.expect_create_deployment() + .returning(|_, _, _, _| Ok(OperationResult::Completed(succeeded_deployment()))); + mock.expect_get_deployment() + .returning(|_, _, _| Ok(succeeded_deployment())); + mock + } + + /// Read the controller's current binding via the ResourceController trait. + fn current_binding(executor: &SingleControllerExecutor) -> AiBinding { + let controller = executor + .internal_state::() + .expect("controller is AzureAiController"); + let value = controller + .get_binding_params() + .expect("binding params resolve") + .expect("binding present once endpoint + account are set"); + serde_json::from_value(value).expect("binding deserializes") + } + + /// A deployment whose model + provisioning state are both reported succeeded. + fn succeeded_deployment() -> CognitiveServicesDeployment { + CognitiveServicesDeployment { + sku: None, + properties: Some(CognitiveServicesDeploymentProperties { + model: CognitiveServicesDeploymentModel { + format: "OpenAI".to_string(), + name: "gpt-4.1".to_string(), + version: "2025-04-14".to_string(), + }, + provisioning_state: Some("Succeeded".to_string()), + }), + } + } + + fn setup_mock_provider_for_deletion(expect_not_found: bool) -> Arc { + let mut mock_provider = MockPlatformServiceProvider::new(); + + mock_provider + .expect_get_azure_cognitive_services_client() + .returning(move |_| { + let mut mock_cognitive = MockCognitiveServicesAccountsApi::new(); + + if expect_not_found { + // Simulate account already deleted. + mock_cognitive + .expect_delete_account() + .returning(|_, _| { + Err(AlienError::new(CloudClientErrorData::RemoteResourceNotFound { + resource_type: "CognitiveServicesAccount".to_string(), + resource_name: "my-ai".to_string(), + })) + }); + } else { + // Successful delete then polling confirms deletion. + mock_cognitive + .expect_delete_account() + .returning(|_, _| Ok(())); + mock_cognitive + .expect_get_account() + .returning(|_, _| { + Err(AlienError::new(CloudClientErrorData::RemoteResourceNotFound { + resource_type: "CognitiveServicesAccount".to_string(), + resource_name: "my-ai".to_string(), + })) + }); + } + + Ok(Arc::new(mock_cognitive)) + }); + + Arc::new(mock_provider) + } + + #[tokio::test] + async fn test_deploys_predefined_models_then_ready() { + // Account already provisioned; drive DeployingModels -> WaitingForDeployments + // -> Ready with create_deployment + get_deployment(Succeeded) mocked. + let mut mock_provider = MockPlatformServiceProvider::new(); + mock_provider + .expect_get_azure_cognitive_services_client() + .returning(move |_| { + let mut mock_cognitive = MockCognitiveServicesAccountsApi::new(); + mock_cognitive + .expect_create_deployment() + .returning(|_, _, _, _| Ok(OperationResult::Completed(succeeded_deployment()))); + mock_cognitive + .expect_get_deployment() + .returning(|_, _, _| Ok(succeeded_deployment())); + Ok(Arc::new(mock_cognitive)) + }); + + let mut executor = SingleControllerExecutor::builder() + .resource(basic_ai()) + .controller(AzureAiController::mock_deploying( + "my-ai-account", + "https://my-ai.cognitiveservices.azure.com/", + )) + .platform(Platform::Azure) + .service_provider(Arc::new(mock_provider)) + .build() + .await + .unwrap(); + + // Step through the deployment states; Ready is not terminal, so stop once + // the controller reports Running. + for _ in 0..5 { + if executor.status() == ResourceStatus::Running { + break; + } + executor.step().await.unwrap(); + } + + assert_eq!( + executor.status(), + ResourceStatus::Running, + "controller should reach Ready after deploying the predefined models" + ); + } + + #[tokio::test] + async fn test_deployment_terminal_failure_fails_fast() { + // A deployment that reports a terminal "Failed" state must route to + // CreateFailed (ProvisionFailed), not poll forever. + let mut mock_provider = MockPlatformServiceProvider::new(); + mock_provider + .expect_get_azure_cognitive_services_client() + .returning(move |_| { + let mut mock_cognitive = MockCognitiveServicesAccountsApi::new(); + mock_cognitive + .expect_create_deployment() + .returning(|_, _, _, _| Ok(OperationResult::Completed(succeeded_deployment()))); + mock_cognitive.expect_get_deployment().returning(|_, _, _| { + Ok(CognitiveServicesDeployment { + sku: None, + properties: Some(CognitiveServicesDeploymentProperties { + model: CognitiveServicesDeploymentModel { + format: "OpenAI".to_string(), + name: "gpt-4.1".to_string(), + version: "2025-04-14".to_string(), + }, + provisioning_state: Some("Failed".to_string()), + }), + }) + }); + Ok(Arc::new(mock_cognitive)) + }); + + let mut executor = SingleControllerExecutor::builder() + .resource(basic_ai()) + .controller(AzureAiController::mock_deploying( + "my-ai-account", + "https://my-ai.cognitiveservices.azure.com/", + )) + .platform(Platform::Azure) + .service_provider(Arc::new(mock_provider)) + .build() + .await + .unwrap(); + + // A "Failed" deployment must surface as a handler error (which the executor + // routes to CreateFailed), not an endless poll. Bounded so a regression + // (poll-forever) fails the test instead of hanging. + let mut surfaced_error = false; + for _ in 0..5 { + if executor.step().await.is_err() { + surfaced_error = true; + break; + } + } + + assert!( + surfaced_error, + "a terminal deployment failure must surface as an error, not a silent retry" + ); + } + + #[tokio::test] + async fn test_ready_controller_has_correct_outputs() { + let account_name = "my-ai-account"; + let endpoint = "https://my-ai.cognitiveservices.azure.com/"; + + let mock_provider = setup_mock_provider_for_deletion(false); + let executor = SingleControllerExecutor::builder() + .resource(basic_ai()) + .controller(AzureAiController::mock_ready(account_name, endpoint)) + .platform(Platform::Azure) + .service_provider(mock_provider) + .build() + .await + .unwrap(); + + assert_eq!(executor.status(), ResourceStatus::Running); + + let outputs = executor.outputs().unwrap(); + let ai_outputs = outputs.downcast_ref::().unwrap(); + assert_eq!(ai_outputs.provider, "foundry"); + assert_eq!(ai_outputs.endpoint.as_deref(), Some(endpoint)); + assert_eq!(ai_outputs.account.as_deref(), Some(account_name)); + } + + #[tokio::test] + async fn test_delete_flow_succeeds() { + let account_name = "my-ai-account"; + let endpoint = "https://my-ai.cognitiveservices.azure.com/"; + + let mock_provider = setup_mock_provider_for_deletion(false); + let mut executor = SingleControllerExecutor::builder() + .resource(basic_ai()) + .controller(AzureAiController::mock_ready(account_name, endpoint)) + .platform(Platform::Azure) + .service_provider(mock_provider) + .build() + .await + .unwrap(); + + executor.delete().unwrap(); + executor.run_until_terminal().await.unwrap(); + + assert_eq!(executor.status(), ResourceStatus::Deleted); + } + + #[tokio::test] + async fn test_delete_already_gone_succeeds() { + // Deleting when the account is already gone (RemoteResourceNotFound on delete_account) + // must succeed, not fail. This is the best-effort-delete path. + let account_name = "my-ai-account"; + let endpoint = "https://my-ai.cognitiveservices.azure.com/"; + + let mock_provider = setup_mock_provider_for_deletion(true); + let mut executor = SingleControllerExecutor::builder() + .resource(basic_ai()) + .controller(AzureAiController::mock_ready(account_name, endpoint)) + .platform(Platform::Azure) + .service_provider(mock_provider) + .build() + .await + .unwrap(); + + executor.delete().unwrap(); + executor.run_until_terminal().await.unwrap(); + + assert_eq!(executor.status(), ResourceStatus::Deleted); + } + + // ─────────────── FINE-TUNING TESTS ────────────────────────── + + #[tokio::test] + async fn test_finetune_reaches_ready_immediately_with_capability() { + // Fine-tuning is triggered at runtime through the gateway, so a finetune + // resource reaches Ready as soon as the base deployments succeed — no + // tuning job or tuned deployment is created at deploy time. The Foundry + // finetuning client is never wired, so a call to it would panic on the + // unset expectation, catching any regression that re-introduces + // deploy-time tuning. The binding carries the FinetuneCapability instead. + let mut mock_provider = MockPlatformServiceProvider::new(); + mock_provider + .expect_get_azure_cognitive_services_client() + .returning(|_| Ok(Arc::new(mock_cognitive_all_succeed()))); + + let mut executor = SingleControllerExecutor::builder() + .resource(tuned_ai()) + .controller(AzureAiController::mock_deploying( + "default-storage-account", + "https://my-ai.cognitiveservices.azure.com/", + )) + .platform(Platform::Azure) + .service_provider(Arc::new(mock_provider)) + .with_test_dependencies() + .with_dependency( + training_storage(), + AzureStorageController::mock_ready(TRAINING_STORAGE_ID), + ) + .build() + .await + .expect("executor builds"); + + // Ready is a heartbeat loop (not terminal), so step until Running. + for _ in 0..8 { + if executor.status() == ResourceStatus::Running { + break; + } + executor.step().await.expect("step should not error"); + } + assert_eq!( + executor.status(), + ResourceStatus::Running, + "a finetune resource is Ready immediately; the gateway triggers tuning at runtime" + ); + + let binding = current_binding(&executor); + assert!( + binding.tuned_model().is_none(), + "the controller must not attach a tuned model; the gateway rediscovers it" + ); + let cap = binding + .finetune() + .expect("finetune binding must carry the fine-tuning capability"); + assert_eq!(cap.base_model, "gpt-4o-mini"); + // Bucket resolved from the storage dependency's container name + // (test-stack-, from AzureStorageController::mock_ready), not + // re-derived here. + assert_eq!(cap.training_bucket, "test-stack-training-set"); + assert_eq!(cap.training_key, "training.jsonl"); + assert_eq!(cap.served_model_id, "my-ai-tuned"); + // Deterministic {prefix}-{id}; the executor test harness uses the + // resource prefix "test" (distinct from the storage mock's "test-stack" + // container-name prefix). + assert_eq!(cap.job_name, "test-my-ai"); + assert!( + cap.role_arn.is_empty(), + "Foundry submits under the ambient identity; no role is passed" + ); + } + + #[tokio::test] + async fn test_no_finetune_reaches_ready_with_plain_binding() { + // Regression: an inference-only resource reaches Ready with a plain + // binding carrying neither a tuned model nor a finetune capability. + let mut mock_provider = MockPlatformServiceProvider::new(); + mock_provider + .expect_get_azure_cognitive_services_client() + .returning(|_| Ok(Arc::new(mock_cognitive_all_succeed()))); + + let mut executor = SingleControllerExecutor::builder() + .resource(basic_ai()) + .controller(AzureAiController::mock_deploying( + "my-ai-account", + "https://my-ai.cognitiveservices.azure.com/", + )) + .platform(Platform::Azure) + .service_provider(Arc::new(mock_provider)) + .with_test_dependencies() + .build() + .await + .expect("executor builds"); + + for _ in 0..6 { + if executor.status() == ResourceStatus::Running { + break; + } + executor.step().await.expect("step should not error"); + } + assert_eq!( + executor.status(), + ResourceStatus::Running, + "inference-only resource must reach Ready" + ); + + let binding = current_binding(&executor); + assert!( + binding.tuned_model().is_none(), + "inference-only binding must omit tuned_model" + ); + assert!( + binding.finetune().is_none(), + "inference-only binding must omit the finetune capability" + ); + } +} diff --git a/crates/alien-infra/src/ai/azure_import.rs b/crates/alien-infra/src/ai/azure_import.rs new file mode 100644 index 000000000..d52458e7e --- /dev/null +++ b/crates/alien-infra/src/ai/azure_import.rs @@ -0,0 +1,38 @@ +//! Importer for Azure AI (AIServices / Azure AI Foundry). + +use alien_core::{ + import::{data::AzureAiImportData, ImportContext}, + Result, StackResourceState, +}; + +use crate::ai::azure::AzureAiState; +use crate::ai::AzureAiController; +use crate::import::ResourceImporter; +use crate::import_helpers::make_imported_state; + +/// Azure AI importer. +#[derive(Debug, Default)] +pub struct AzureAiImporter; + +impl ResourceImporter for AzureAiImporter { + type ImportData = AzureAiImportData; + + fn import( + &self, + data: AzureAiImportData, + ctx: &ImportContext<'_>, + ) -> Result { + let controller = AzureAiController { + state: AzureAiState::Ready, + account_name: Some(data.account_name), + endpoint: Some(data.endpoint), + resource_group: Some(data.resource_group), + location: Some(data.location), + // Import targets an existing base gateway; it advertises no + // fine-tuning capability. + finetune: None, + _internal_stay_count: None, + }; + make_imported_state(controller, ctx) + } +} diff --git a/crates/alien-infra/src/ai/gcp.rs b/crates/alien-infra/src/ai/gcp.rs new file mode 100644 index 000000000..a218bd759 --- /dev/null +++ b/crates/alien-infra/src/ai/gcp.rs @@ -0,0 +1,604 @@ +use std::time::Duration; + +use tracing::info; + +use crate::core::{ResourceControllerContext, ResourcePermissionsHelper}; +use crate::error::{ErrorData, Result}; +use crate::storage::GcpStorageController; +use alien_core::{ + bindings::{AiBinding, FinetuneCapability}, + Ai, AiHeartbeatData, AiHeartbeatStatus, AiOutputs, GcpVertexAiHeartbeatData, HeartbeatBackend, + Platform, ResourceHeartbeat, ResourceHeartbeatData, ResourceOutputs, ResourceRef, + ResourceStatus, Storage, +}; +use alien_error::{AlienError, Context, IntoAlienError}; +use alien_gcp_clients::iam::IamPolicy; +use alien_gcp_clients::resource_manager::GetPolicyOptions; +use alien_macros::controller; +use chrono::Utc; + +#[controller] +pub struct GcpAiController { + /// GCP project ID. None until create_start runs. + pub(crate) project: Option, + /// GCP region (location) for the Vertex AI endpoint. None until create_start runs. + pub(crate) location: Option, + /// The fine-tuning capability this gateway advertises, resolved during the + /// create flow when the resource declares a `finetune` spec. The gateway + /// submits and rediscovers the tuning job at runtime from this capability; + /// the controller never starts a job itself. Captured into state (rather than + /// rebuilt in `get_binding_params`, which has no `ctx`) so the binding can + /// carry it. `None` for a pure-inference gateway. + #[serde(default)] + pub(crate) finetune: Option, +} + +#[controller] +impl GcpAiController { + // ─────────────── CREATE FLOW ────────────────────────────── + + #[flow_entry(Create)] + #[handler( + state = CreateStart, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn create_start( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let gcp_config = ctx.get_gcp_config()?; + let config = ctx.desired_resource_config::()?; + + self.project = Some(gcp_config.project_id.clone()); + self.location = Some(gcp_config.region.clone()); + + info!( + id = %config.id, + project = %gcp_config.project_id, + location = %gcp_config.region, + "GCP AI (Vertex AI) controller: enabling API" + ); + + Ok(HandlerAction::Continue { + state: EnablingApi, + suggested_delay: None, + }) + } + + #[handler( + state = EnablingApi, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn enabling_api( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let gcp_config = ctx.get_gcp_config()?; + let config = ctx.desired_resource_config::()?; + let client = ctx + .service_provider + .get_gcp_service_usage_client(gcp_config)?; + + info!( + id = %config.id, + project = %gcp_config.project_id, + "Enabling aiplatform.googleapis.com API" + ); + + client + .enable_service("aiplatform.googleapis.com".to_string()) + .await + .context(ErrorData::CloudPlatformError { + message: "Failed to enable aiplatform.googleapis.com".to_string(), + resource_id: Some(config.id.clone()), + })?; + + info!( + id = %config.id, + "aiplatform.googleapis.com enabled (or already enabled)" + ); + + Ok(HandlerAction::Continue { + state: ApplyingResourcePermissions, + suggested_delay: None, + }) + } + + #[handler( + state = ApplyingResourcePermissions, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn applying_resource_permissions( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let gcp_config = ctx.get_gcp_config()?; + let config = ctx.desired_resource_config::()?; + let rm_client = ctx + .service_provider + .get_gcp_resource_manager_client(gcp_config)?; + let project_id = gcp_config.project_id.clone(); + let config_id = config.id.clone(); + + info!( + id = %config.id, + project = %project_id, + "Applying Vertex AI resource-scoped permissions (custom predict-only role)" + ); + + ResourcePermissionsHelper::apply_gcp_resource_scoped_permissions( + ctx, + &config.id, + &config.id, + "GCP AI", + "ai", + rm_client, + |rm_client, desired_policy| async move { + // Project-level IAM requires read-modify-write to avoid clobbering + // bindings owned by other controllers. + let current_policy = rm_client + .get_project_iam_policy( + project_id.clone(), + Some(GetPolicyOptions { + requested_policy_version: Some(3), + }), + ) + .await + .context(ErrorData::CloudPlatformError { + message: "Failed to get project IAM policy before applying AI permissions" + .to_string(), + resource_id: Some(config_id.clone()), + })?; + + let owned_exact_roles = + ResourcePermissionsHelper::gcp_predefined_role_names(&desired_policy.bindings); + let mut all_bindings = current_policy.bindings; + + // Reconcile each member/role binding separately so we only touch what + // belongs to this stack's workload service accounts. + for desired_binding in &desired_policy.bindings { + for member in &desired_binding.members { + ResourcePermissionsHelper::reconcile_gcp_project_member_bindings( + &mut all_bindings, + vec![desired_binding.clone()], + member, + &[], + &owned_exact_roles, + ); + } + } + + let new_policy = IamPolicy::builder() + .version(3) + .bindings(all_bindings) + .maybe_etag(current_policy.etag) + .maybe_kind(current_policy.kind) + .maybe_resource_id(current_policy.resource_id) + .build(); + + rm_client + .set_project_iam_policy(project_id.clone(), new_policy, None) + .await + .context(ErrorData::CloudPlatformError { + message: "Failed to set project IAM policy for Vertex AI".to_string(), + resource_id: Some(config_id.clone()), + })?; + + info!( + project = %project_id, + "Applied the custom predict-only role at project scope for Vertex AI" + ); + + Ok(()) + }, + ) + .await?; + + // Fine-tuning is triggered at runtime through the gateway, not at deploy + // time, so the resource is Ready as soon as permissions are applied. When + // the resource declares a `finetune` spec, resolve the capability the + // gateway needs to submit/rediscover a runtime tuning job and carry it on + // the binding. + self.finetune = self.resolve_finetune_capability(ctx, &config)?; + + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── READY STATE ──────────────────────────────── + // Loops as a heartbeat tick; Vertex AI has no per-stack resource to poll. + + #[handler( + state = Ready, + on_failure = RefreshFailed, + status = ResourceStatus::Running, + )] + async fn ready(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + let project = self.project.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + resource_id: Some(config.id.clone()), + message: "Project not set in state".to_string(), + }) + })?; + let location = self.location.as_ref().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + resource_id: Some(config.id.clone()), + message: "Location not set in state".to_string(), + }) + })?; + info!(id = %config.id, "GCP AI heartbeat tick"); + ctx.emit_heartbeat(ResourceHeartbeat { + deployment_id: None, + resource_id: config.id.clone(), + resource_type: Ai::RESOURCE_TYPE, + controller_platform: Platform::Gcp, + backend: HeartbeatBackend::Gcp, + observed_at: Utc::now(), + data: ResourceHeartbeatData::Ai(AiHeartbeatData::GcpVertex( + GcpVertexAiHeartbeatData { + status: AiHeartbeatStatus::default(), + project: project.clone(), + location: location.clone(), + }, + )), + raw: vec![], + }); + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: Some(Duration::from_secs(30)), + }) + } + + // ─────────────── UPDATE FLOW ────────────────────────────── + // Ai has no mutable fields -- update is a no-op that also recovers RefreshFailed. + + #[flow_entry(Update, from = [Ready, RefreshFailed])] + #[handler( + state = UpdateStart, + on_failure = UpdateFailed, + status = ResourceStatus::Updating, + )] + async fn update_start( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let config = ctx.desired_resource_config::()?; + info!(id = %config.id, "GCP AI update (no-op -- no mutable fields)"); + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── DELETE FLOW ────────────────────────────── + // GCP AI creates no cloud resource; deletion is always a no-op. + // The shared aiplatform API is not disabled on delete (other stacks may use it). + + #[flow_entry(Delete)] + #[handler( + state = DeleteStart, + on_failure = DeleteFailed, + status = ResourceStatus::Deleting, + )] + async fn delete_start( + &mut self, + ctx: &ResourceControllerContext<'_>, + ) -> Result { + let config = ctx.desired_resource_config::()?; + info!( + id = %config.id, + "GCP AI delete (no-op -- Vertex AI has no per-stack resource to remove)" + ); + Ok(HandlerAction::Continue { + state: Deleted, + suggested_delay: None, + }) + } + + // ─────────────── TERMINALS ──────────────────────────────── + + terminal_state!( + state = CreateFailed, + status = ResourceStatus::ProvisionFailed + ); + + terminal_state!(state = UpdateFailed, status = ResourceStatus::UpdateFailed); + + terminal_state!(state = DeleteFailed, status = ResourceStatus::DeleteFailed); + + terminal_state!( + state = RefreshFailed, + status = ResourceStatus::RefreshFailed + ); + + terminal_state!(state = Deleted, status = ResourceStatus::Deleted); + + fn build_outputs(&self) -> Option { + let project = self.project.as_ref()?; + let location = self.location.as_ref()?; + Some(ResourceOutputs::new(AiOutputs { + provider: "vertex".into(), + endpoint: Some(format!( + "https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi" + )), + account: None, + })) + } + + fn get_binding_params(&self) -> Result> { + // Not-ready is Ok(None), not Err: the executor calls this on every step + // commit (including the pre-provision path), and routing a missing + // project/location through the error channel would corrupt its retry + // accounting. Err is reserved for a real serialization failure below. + let (Some(project), Some(location)) = (self.project.as_ref(), self.location.as_ref()) + else { + return Ok(None); + }; + + let mut binding = AiBinding::vertex(project, location); + + // Carry the fine-tuning capability when the resource declared one, so the + // gateway can submit/rediscover a runtime tuning job. A pure-inference + // gateway returns the untuned binding unchanged. The controller never + // attaches a `tuned_model` — the gateway rediscovers it by convention. + if let Some(capability) = self.finetune.as_ref() { + binding = binding.with_finetune(capability.clone()); + } + + Ok(Some(serde_json::to_value(binding).into_alien_error().context( + ErrorData::ResourceStateSerializationFailed { + resource_id: "binding".to_string(), + message: "Failed to serialize AI binding parameters".to_string(), + }, + )?)) + } +} + +impl GcpAiController { + /// Builds the fine-tuning capability the binding carries when the resource + /// declares a `finetune` spec, or `None` for a pure-inference gateway. + /// + /// Resolves the training data's real GCS bucket from the storage dependency's + /// controller state (rather than re-deriving the prefixed name), keeping it in + /// lockstep with the storage controller's naming. Vertex submits the runtime + /// tuning job under the gateway's ambient identity, so no role is passed + /// (`role_arn` is empty). + fn resolve_finetune_capability( + &self, + ctx: &ResourceControllerContext<'_>, + config: &Ai, + ) -> Result> { + let Some(spec) = config.finetune.as_ref() else { + return Ok(None); + }; + + let training_ref = ResourceRef::new(Storage::RESOURCE_TYPE, spec.training_data.clone()); + let storage_state = ctx.require_dependency::(&training_ref)?; + let training_bucket = storage_state.bucket_name.ok_or_else(|| { + AlienError::new(ErrorData::DependencyNotReady { + resource_id: config.id.clone(), + dependency_id: spec.training_data.clone(), + }) + })?; + + Ok(Some(FinetuneCapability { + base_model: spec.base_model.clone(), + training_bucket, + training_key: spec.training_key.clone(), + served_model_id: spec.served_model_id_or_default(&config.id), + job_name: format!("{}-{}", ctx.resource_prefix, config.id), + // Vertex submits under the ambient identity; no passed role. + role_arn: String::new(), + })) + } +} + +#[cfg(test)] +mod tests { + //! GCP Vertex AI controller tests. + //! + //! Fine-tuning is triggered at runtime through the gateway, not at deploy + //! time, so the controller reaches Ready immediately and never submits a + //! Vertex tuning job. These assert that a finetune resource reaches Ready + //! with no job created and that its binding carries the `FinetuneCapability` + //! the gateway needs, plus the pure-inference regression (no finetune -> + //! Ready with a plain binding). + + use std::sync::Arc; + + use super::GcpAiController; + use alien_core::bindings::AiBinding; + use alien_core::{Ai, FinetuneMethod, FinetuneSpec, Platform, ResourceStatus, Storage}; + use alien_gcp_clients::iam::IamPolicy; + use alien_gcp_clients::longrunning::Operation; + use alien_gcp_clients::resource_manager::MockResourceManagerApi; + use alien_gcp_clients::service_usage::MockServiceUsageApi; + + use crate::core::controller_test::SingleControllerExecutor; + use crate::core::{MockPlatformServiceProvider, PlatformServiceProvider, ResourceController}; + use crate::storage::GcpStorageController; + + const TRAINING_STORAGE_ID: &str = "training-set"; + + // ─────────────── FIXTURES ────────────────────────────────── + + /// A pure-inference gateway (no finetune). + fn base_ai() -> Ai { + Ai::new("llm".to_string()).build() + } + + /// An Ai that declares a fine-tuning capability over the training storage. + fn finetune_ai() -> Ai { + Ai::new("llm".to_string()) + .finetune(FinetuneSpec { + base_model: "gemini-2.0-flash-001".to_string(), + training_data: TRAINING_STORAGE_ID.to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }) + .build() + } + + fn training_storage() -> Storage { + Storage::new(TRAINING_STORAGE_ID.to_string()).build() + } + + // ─────────────── MOCK HELPERS ────────────────────────────── + + /// Resource-manager mock that satisfies the read-modify-write IAM step in + /// `applying_resource_permissions` (the AI resource grants no bindings, but + /// the closure always gets then sets the project policy). + fn iam_mock() -> Arc { + let mut mock = MockResourceManagerApi::new(); + mock.expect_get_project_iam_policy() + .returning(|_, _| Ok(IamPolicy::default())); + mock.expect_set_project_iam_policy() + .returning(|_, policy, _| Ok(policy)); + Arc::new(mock) + } + + fn service_usage_mock() -> Arc { + let mut mock = MockServiceUsageApi::new(); + mock.expect_enable_service() + .returning(|_| Ok(Operation::default())); + Arc::new(mock) + } + + /// Wires a service provider with the service-usage and resource-manager + /// mocks the create flow needs. The controller no longer submits a Vertex + /// tuning job, so no aiplatform client is wired — a call to it would panic on + /// the unset expectation, catching any regression that re-introduces + /// deploy-time tuning. + fn provider() -> Arc { + let mut provider = MockPlatformServiceProvider::new(); + provider + .expect_get_gcp_service_usage_client() + .returning({ + let m = service_usage_mock(); + move |_| Ok(m.clone()) + }); + provider + .expect_get_gcp_resource_manager_client() + .returning({ + let m = iam_mock(); + move |_| Ok(m.clone()) + }); + Arc::new(provider) + } + + // ─────────────── TESTS ───────────────────────────────────── + + /// A finetune resource reaches Ready immediately (no tuning job submitted at + /// deploy) and its binding carries the `FinetuneCapability` with the bucket + /// resolved from the storage dependency, no `tuned_model`, and an empty + /// `role_arn` (Vertex submits under the ambient identity). + #[tokio::test] + async fn finetune_reaches_ready_immediately_with_capability() { + let mut executor = SingleControllerExecutor::builder() + .resource(finetune_ai()) + .controller(GcpAiController::default()) + .platform(Platform::Gcp) + .service_provider(provider()) + .with_dependency( + training_storage(), + GcpStorageController::mock_ready(TRAINING_STORAGE_ID), + ) + .build() + .await + .expect("executor should build"); + + executor + .run_until_terminal() + .await + .expect("finetune create flow should complete"); + + assert_eq!( + executor.status(), + ResourceStatus::Running, + "a finetune resource is Ready immediately; the gateway triggers tuning at runtime" + ); + + let controller = executor + .internal_state::() + .expect("controller downcast"); + + // The binding the gateway consumes carries the finetune capability but no + // tuned model (the gateway rediscovers the tuned model by convention). + let params = controller + .get_binding_params() + .expect("binding params ok") + .expect("binding present once project/location are set"); + let binding: AiBinding = serde_json::from_value(params).expect("binding deserializes"); + assert!( + binding.tuned_model().is_none(), + "the controller must not attach a tuned model; the gateway rediscovers it" + ); + let cap = binding + .finetune() + .expect("finetune binding must carry the fine-tuning capability"); + assert_eq!(cap.base_model, "gemini-2.0-flash-001"); + // Bucket resolved from the storage dependency (test-stack-, from + // GcpStorageController::mock_ready), not re-derived here. + assert_eq!(cap.training_bucket, "test-stack-training-set"); + assert_eq!(cap.training_key, "training.jsonl"); + assert_eq!(cap.served_model_id, "llm-tuned"); + // Deterministic {prefix}-{id}; the executor test harness uses the + // resource prefix "test" (distinct from the storage mock's "test-stack" + // bucket-name prefix). + assert_eq!(cap.job_name, "test-llm"); + assert!( + cap.role_arn.is_empty(), + "Vertex submits under the ambient identity; no role is passed" + ); + } + + /// Regression: an Ai without a finetune spec reaches Ready with a plain + /// binding carrying neither a tuned model nor a finetune capability. + #[tokio::test] + async fn no_finetune_reaches_ready_with_plain_binding() { + let mut executor = SingleControllerExecutor::builder() + .resource(base_ai()) + .controller(GcpAiController::default()) + .platform(Platform::Gcp) + .service_provider(provider()) + .build() + .await + .expect("executor should build"); + + executor + .run_until_terminal() + .await + .expect("inference create flow should complete"); + + assert_eq!(executor.status(), ResourceStatus::Running); + + let controller = executor + .internal_state::() + .expect("controller downcast"); + assert!( + controller.finetune.is_none(), + "a pure-inference gateway advertises no fine-tuning capability" + ); + + let params = controller + .get_binding_params() + .expect("binding params ok") + .expect("binding present"); + let binding: AiBinding = serde_json::from_value(params).expect("binding deserializes"); + assert!( + binding.tuned_model().is_none(), + "an inference-only gateway must not carry a tuned model" + ); + assert!( + binding.finetune().is_none(), + "an inference-only gateway must not carry a finetune capability" + ); + } +} diff --git a/crates/alien-infra/src/ai/gcp_import.rs b/crates/alien-infra/src/ai/gcp_import.rs new file mode 100644 index 000000000..635d58109 --- /dev/null +++ b/crates/alien-infra/src/ai/gcp_import.rs @@ -0,0 +1,39 @@ +//! Importer for GCP AI (Vertex AI). +//! +//! GCP AI creates no cloud resource; the importer carries project and location +//! from the import payload directly into the controller state. + +use alien_core::{ + import::{data::GcpAiImportData, ImportContext}, + Result, StackResourceState, +}; + +use crate::ai::gcp::GcpAiState; +use crate::ai::GcpAiController; +use crate::import::ResourceImporter; +use crate::import_helpers::make_imported_state; + +/// GCP Vertex AI importer. +#[derive(Debug, Default)] +pub struct GcpAiImporter; + +impl ResourceImporter for GcpAiImporter { + type ImportData = GcpAiImportData; + + fn import( + &self, + data: GcpAiImportData, + ctx: &ImportContext<'_>, + ) -> Result { + let controller = GcpAiController { + state: GcpAiState::Ready, + project: Some(data.project_id), + location: Some(data.location), + // Import targets an existing base gateway; it advertises no + // fine-tuning capability. + finetune: None, + _internal_stay_count: None, + }; + make_imported_state(controller, ctx) + } +} diff --git a/crates/alien-infra/src/ai/local.rs b/crates/alien-infra/src/ai/local.rs new file mode 100644 index 000000000..3a06697b7 --- /dev/null +++ b/crates/alien-infra/src/ai/local.rs @@ -0,0 +1,203 @@ +use std::time::Duration; + +use tracing::info; + +use crate::core::ResourceControllerContext; +use crate::error::{ErrorData, Result}; +use alien_core::{ + bindings::AiBinding, Ai, AiHeartbeatData, AiHeartbeatStatus, AiOutputs, ExternalAiHeartbeatData, + HeartbeatBackend, Platform, ResourceHeartbeat, ResourceHeartbeatData, ResourceOutputs, + ResourceStatus, +}; +use alien_error::{AlienError, Context, IntoAlienError}; +use alien_macros::controller; +use chrono::Utc; + +/// The env var the developer sets to bring their own provider key for local dev. +const API_KEY_ENV: &str = AiBinding::LOCAL_API_KEY_ENV; +/// The default provider when running locally; the SDK maps it to the provider's +/// OpenAI-compatible base URL (overridable via `ALIEN_AI_LOCAL_BASE_URL`). +const DEFAULT_PROVIDER: &str = AiBinding::LOCAL_DEFAULT_PROVIDER; + +/// Local AI controller. There is no cloud LLM to provision and no ambient identity on +/// a developer's machine, so the developer *brings their own key* (`OPENAI_API_KEY`) and +/// the app calls the provider directly. `get_binding_params` syncs the key-less binding +/// coordinates; the key itself reaches a linked workload through the runtime-only channel +/// (`LocalBindingsProvider::resolve_runtime_only_binding_env`), so it never enters +/// persisted or control-plane-synced state. +#[controller] +pub struct LocalAiController { + /// The BYO-key provider (e.g. "openai"). None until create_start runs. + pub(crate) provider: Option, +} + +#[controller] +impl LocalAiController { + // ─────────────── CREATE FLOW ────────────────────────────── + + #[flow_entry(Create)] + #[handler( + state = CreateStart, + on_failure = CreateFailed, + status = ResourceStatus::Provisioning, + )] + async fn create_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + + // Fail loud if the developer hasn't supplied a key — local AI has no ambient + // identity to fall back on, so this is a required, actionable precondition. + if std::env::var(API_KEY_ENV).map(|k| k.is_empty()).unwrap_or(true) { + return Err(AlienError::new(ErrorData::ResourceConfigInvalid { + resource_id: Some(config.id.clone()), + message: format!( + "local AI resource '{}' needs a provider API key: set {API_KEY_ENV} before `alien dev`", + config.id + ), + })); + } + + self.provider = Some(DEFAULT_PROVIDER.to_string()); + info!(id=%config.id, provider=DEFAULT_PROVIDER, "Local AI (BYO-key) controller: no resource to create"); + + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── READY STATE ──────────────────────────────── + // Loops as a heartbeat tick; there is no per-stack resource to poll. + + #[handler( + state = Ready, + on_failure = RefreshFailed, + status = ResourceStatus::Running, + )] + async fn ready(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + let provider = self.provider.clone().ok_or_else(|| { + AlienError::new(ErrorData::ResourceConfigInvalid { + resource_id: Some(config.id.clone()), + message: "Provider not set in state".to_string(), + }) + })?; + ctx.emit_heartbeat(ResourceHeartbeat { + deployment_id: None, + resource_id: config.id.clone(), + resource_type: Ai::RESOURCE_TYPE, + controller_platform: Platform::Local, + backend: HeartbeatBackend::Local, + observed_at: Utc::now(), + data: ResourceHeartbeatData::Ai(AiHeartbeatData::External(ExternalAiHeartbeatData { + status: AiHeartbeatStatus::default(), + provider, + })), + raw: vec![], + }); + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: Some(Duration::from_secs(30)), + }) + } + + // ─────────────── UPDATE FLOW ────────────────────────────── + // Ai has no mutable fields -- update is a no-op that also recovers RefreshFailed. + + #[flow_entry(Update, from = [Ready, RefreshFailed])] + #[handler( + state = UpdateStart, + on_failure = UpdateFailed, + status = ResourceStatus::Updating, + )] + async fn update_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + info!(id=%config.id, "Local AI update (no-op -- no mutable fields)"); + Ok(HandlerAction::Continue { + state: Ready, + suggested_delay: None, + }) + } + + // ─────────────── DELETE FLOW ────────────────────────────── + // No cloud resource is created; deletion is always a no-op. + + #[flow_entry(Delete)] + #[handler( + state = DeleteStart, + on_failure = DeleteFailed, + status = ResourceStatus::Deleting, + )] + async fn delete_start(&mut self, ctx: &ResourceControllerContext<'_>) -> Result { + let config = ctx.desired_resource_config::()?; + info!(id=%config.id, "Local AI delete (no-op -- nothing was provisioned)"); + Ok(HandlerAction::Continue { + state: Deleted, + suggested_delay: None, + }) + } + + // ─────────────── TERMINALS ──────────────────────────────── + + terminal_state!(state = CreateFailed, status = ResourceStatus::ProvisionFailed); + terminal_state!(state = UpdateFailed, status = ResourceStatus::UpdateFailed); + terminal_state!(state = DeleteFailed, status = ResourceStatus::DeleteFailed); + terminal_state!(state = RefreshFailed, status = ResourceStatus::RefreshFailed); + terminal_state!(state = Deleted, status = ResourceStatus::Deleted); + + fn build_outputs(&self) -> Option { + let provider = self.provider.as_ref()?; + Some(ResourceOutputs::new(AiOutputs { + provider: provider.clone(), + endpoint: None, + account: None, + })) + } + + fn get_binding_params(&self) -> Result> { + // Synced channel (control-plane `remote_binding_params`): emit the provider only, + // with the key stripped so a BYO-key never reaches synced/persisted state. The + // gateway reads the key from the never-synced worker-env channel below. + let provider = match &self.provider { + Some(p) => p, + None => return Ok(None), + }; + let mut value = serde_json::to_value(AiBinding::external(provider.clone(), String::new())) + .into_alien_error() + .context(ErrorData::ResourceStateSerializationFailed { + resource_id: "binding".to_string(), + message: "Failed to serialize AI binding parameters".to_string(), + })?; + if let Some(obj) = value.as_object_mut() { + obj.remove("apiKey"); + } + Ok(Some(value)) + } + +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::ResourceController; + + #[test] + fn synced_binding_carries_the_provider_but_never_the_key() { + // The control-plane-synced channel must expose the provider (so the resource's + // shape is visible) while stripping the BYO key -- a secret never belongs in + // synced or persisted state. The worker-env channel (resolve_binding_params) + // is where the key travels, and it is never synced. + let controller = LocalAiController { + provider: Some("openai".to_string()), + ..Default::default() + }; + let value = controller + .get_binding_params() + .expect("binding params serialize") + .expect("a provisioned controller emits a binding"); + assert_eq!(value.get("provider").and_then(|v| v.as_str()), Some("openai")); + assert!( + value.get("apiKey").is_none(), + "the synced binding must never carry the provider key: {value}" + ); + } +} diff --git a/crates/alien-infra/src/ai/mod.rs b/crates/alien-infra/src/ai/mod.rs new file mode 100644 index 000000000..852221051 --- /dev/null +++ b/crates/alien-infra/src/ai/mod.rs @@ -0,0 +1,32 @@ +pub mod aws; +pub use aws::AwsAiController; + +#[cfg(feature = "aws")] +mod aws_import; +#[cfg(feature = "aws")] +pub use aws_import::AwsAiImporter; + +#[cfg(feature = "gcp")] +pub mod gcp; +#[cfg(feature = "gcp")] +pub use gcp::GcpAiController; + +#[cfg(feature = "gcp")] +mod gcp_import; +#[cfg(feature = "gcp")] +pub use gcp_import::GcpAiImporter; + +#[cfg(feature = "azure")] +pub mod azure; +#[cfg(feature = "azure")] +pub use azure::AzureAiController; + +#[cfg(feature = "azure")] +mod azure_import; +#[cfg(feature = "azure")] +pub use azure_import::AzureAiImporter; + +#[cfg(feature = "local")] +pub mod local; +#[cfg(feature = "local")] +pub use local::LocalAiController; diff --git a/crates/alien-infra/src/aws_importers.rs b/crates/alien-infra/src/aws_importers.rs index 7cc96587c..0de60ae47 100644 --- a/crates/alien-infra/src/aws_importers.rs +++ b/crates/alien-infra/src/aws_importers.rs @@ -11,11 +11,12 @@ #[cfg(feature = "kubernetes")] use alien_core::KubernetesCluster; use alien_core::{ - ArtifactRegistry, AwsOpenSearch, Build, Email, Kv, Network, Platform, Queue, Storage, Vault, + Ai, ArtifactRegistry, AwsOpenSearch, Build, Email, Kv, Network, Platform, Queue, Storage, Vault, Worker, }; use alien_core::{RemoteStackManagement, ServiceAccount}; +use crate::ai::AwsAiImporter; use crate::artifact_registry::AwsArtifactRegistryImporter; use crate::build::AwsBuildImporter; use crate::email::AwsEmailImporter; @@ -35,6 +36,7 @@ use crate::ImporterRegistry; /// Register every OSS AWS importer with `registry`. pub fn register(registry: &mut ImporterRegistry) { registry + .register(Ai::RESOURCE_TYPE, Platform::Aws, AwsAiImporter) .register(Storage::RESOURCE_TYPE, Platform::Aws, AwsStorageImporter) .register(Kv::RESOURCE_TYPE, Platform::Aws, AwsKvImporter) .register(Vault::RESOURCE_TYPE, Platform::Aws, AwsVaultImporter) diff --git a/crates/alien-infra/src/azure_importers.rs b/crates/alien-infra/src/azure_importers.rs index a713aeb3b..baec7cd5a 100644 --- a/crates/alien-infra/src/azure_importers.rs +++ b/crates/alien-infra/src/azure_importers.rs @@ -12,11 +12,12 @@ #[cfg(feature = "kubernetes")] use alien_core::KubernetesCluster; use alien_core::{ - ArtifactRegistry, AzureContainerAppsEnvironment, AzureResourceGroup, AzureServiceBusNamespace, - AzureStorageAccount, Build, Kv, Network, Platform, Queue, RemoteStackManagement, - ServiceAccount, ServiceActivation, Storage, Vault, Worker, + Ai, ArtifactRegistry, AzureContainerAppsEnvironment, AzureResourceGroup, + AzureServiceBusNamespace, AzureStorageAccount, Build, Kv, Network, Platform, Queue, + RemoteStackManagement, ServiceAccount, ServiceActivation, Storage, Vault, Worker, }; +use crate::ai::AzureAiImporter; use crate::artifact_registry::AzureArtifactRegistryImporter; use crate::build::AzureBuildImporter; #[cfg(feature = "kubernetes")] @@ -39,6 +40,7 @@ use crate::ImporterRegistry; pub fn register(registry: &mut ImporterRegistry) { registry // Main resources + .register(Ai::RESOURCE_TYPE, Platform::Azure, AzureAiImporter) .register( Storage::RESOURCE_TYPE, Platform::Azure, diff --git a/crates/alien-infra/src/container/local.rs b/crates/alien-infra/src/container/local.rs index 2dcf3e5ca..2f570c06a 100644 --- a/crates/alien-infra/src/container/local.rs +++ b/crates/alien-infra/src/container/local.rs @@ -252,35 +252,40 @@ impl LocalContainerController { // same-name value from the deployment environment snapshot. env_vars.extend(crate::core::direct_monitoring_auth_headers(ctx)); - // Local Postgres inlines its password in the binding (no secret store) and `get_binding_params` - // strips it from the synced copy, so re-resolve the full binding from the manager's 0600 - // metadata straight into the container env. - for link in &config.links { - if link.resource_type() != &Postgres::RESOURCE_TYPE { - continue; - } - let manager = ctx + // A linked binding that carries a runtime-only secret (Postgres password, BYO-key AI) is + // stripped from the synced copy by `get_binding_params`; re-resolve the full binding + // straight into the container env, which is never persisted in Alien state. Unlike the + // worker recover path, at container start the linked resource must exist — fail loud if + // nothing resolves. + for binding_ref in alien_local::RuntimeOnlyBindingRef::from_links(&config.links) { + let bindings_provider = ctx .service_provider - .get_local_postgres_manager() + .get_local_bindings_provider() .ok_or_else(|| { AlienError::new(ErrorData::LocalServicesNotAvailable { - service_name: "postgres_manager".to_string(), + service_name: "bindings_provider".to_string(), }) })?; - let binding = manager.get_binding(link.id()).context( - ErrorData::ResourceControllerConfigError { - resource_id: link.id().to_string(), - message: format!("Failed to read binding for local Postgres '{}'", link.id()), - }, - )?; - env_vars.extend( - alien_core::bindings::serialize_binding_as_env_var(link.id(), &binding).context( - ErrorData::ResourceControllerConfigError { - resource_id: link.id().to_string(), - message: "Failed to serialize Postgres binding".to_string(), - }, - )?, - ); + let entry = bindings_provider + .resolve_runtime_only_binding_env(&binding_ref.name, &binding_ref.resource_type) + .await + .context(ErrorData::ResourceControllerConfigError { + resource_id: binding_ref.name.clone(), + message: format!( + "Failed to resolve runtime-only binding '{}'", + binding_ref.name + ), + })? + .ok_or_else(|| { + AlienError::new(ErrorData::ResourceControllerConfigError { + resource_id: binding_ref.name.clone(), + message: format!( + "runtime-only binding '{}' resolved to nothing", + binding_ref.name + ), + }) + })?; + env_vars.extend(entry); } // Rewrite binding env vars to use container paths instead of host paths diff --git a/crates/alien-infra/src/core/controller.rs b/crates/alien-infra/src/core/controller.rs index b1e4907ce..b4dd32247 100644 --- a/crates/alien-infra/src/core/controller.rs +++ b/crates/alien-infra/src/core/controller.rs @@ -1108,6 +1108,16 @@ fn deserialize_controller_by_tag( deser!(crate::remote_stack_management::TestRemoteStackManagementController) } + // AI controllers + #[cfg(feature = "aws")] + "AwsAiController" => deser!(crate::ai::AwsAiController), + #[cfg(feature = "gcp")] + "GcpAiController" => deser!(crate::ai::GcpAiController), + #[cfg(feature = "azure")] + "AzureAiController" => deser!(crate::ai::AzureAiController), + #[cfg(feature = "local")] + "LocalAiController" => deser!(crate::ai::LocalAiController), + // Service activation controllers #[cfg(feature = "gcp")] "GcpServiceActivationController" => { diff --git a/crates/alien-infra/src/core/environment_variables.rs b/crates/alien-infra/src/core/environment_variables.rs index b85da8388..75a00ff6b 100644 --- a/crates/alien-infra/src/core/environment_variables.rs +++ b/crates/alien-infra/src/core/environment_variables.rs @@ -491,38 +491,38 @@ impl EnvironmentVariableBuilder { // Check if there's an external binding for this resource match ctx.deployment_config.external_bindings.get(binding_name) { Some(external) => { - let value = match external { - alien_core::ExternalBinding::Storage(b) => serde_json::to_value(b), - alien_core::ExternalBinding::Queue(b) => serde_json::to_value(b), - alien_core::ExternalBinding::Kv(b) => serde_json::to_value(b), - alien_core::ExternalBinding::ArtifactRegistry(b) => { - serde_json::to_value(b) - } - alien_core::ExternalBinding::Vault(b) => serde_json::to_value(b), - alien_core::ExternalBinding::ContainerAppsEnvironment(b) => { - serde_json::to_value(b) - } - alien_core::ExternalBinding::Postgres(b) => { - // External (Remote Access) Postgres on the local platform: the - // binding inlines a raw password with no local manager to - // re-resolve it, so it would persist into worker metadata. - if ctx.platform == alien_core::Platform::Local { - return Err(AlienError::new(ErrorData::ResourceConfigInvalid { - message: "external (Remote Access) Postgres is not supported on the local platform".to_string(), - resource_id: Some(binding_name.to_string()), - })); + // External Postgres and AI on the local platform: the binding + // inlines a raw secret, and the local runtime-only channel would + // either persist it into worker metadata (Postgres) or silently + // replace it with the dev key from the process environment (AI). + if ctx.platform == alien_core::Platform::Local + && matches!( + external, + alien_core::ExternalBinding::Postgres(_) + | alien_core::ExternalBinding::Ai(_) + ) + { + let kind = match external { + alien_core::ExternalBinding::Postgres(_) => { + "external (Remote Access) Postgres" } - serde_json::to_value(b) - } + _ => "an external (BYO-key) AI binding", + }; + return Err(AlienError::new(ErrorData::ResourceConfigInvalid { + message: format!( + "{kind} is not supported on the local platform" + ), + resource_id: Some(binding_name.to_string()), + })); } - .into_alien_error() - .context( - ErrorData::ResourceStateSerializationFailed { + let value = external + .to_env_binding_value() + .into_alien_error() + .context(ErrorData::ResourceStateSerializationFailed { resource_id: binding_name.to_string(), message: "Failed to serialize external binding parameters" .to_string(), - }, - )?; + })?; Some(value) } None => None, @@ -659,6 +659,46 @@ mod tests { assert_eq!(bindings[0].1, binding_json); } + /// Real-path assertion for the AI external-binding env-var injection: drives the + /// PRODUCTION serialization (`ExternalBinding::to_env_binding_value`, the same + /// call `add_linked_resources` makes) through `serialize_binding_as_env_var` and + /// asserts the `ALIEN__BINDING` value is the exact service-tagged JSON the + /// SDK's `ai(name)` parser consumes. A drift here (e.g. dropping the + /// `AiBinding::External` wrap) silently routes external OpenAI/Anthropic to the + /// wrong upstream. + #[test] + fn test_ai_external_binding_injects_service_tagged_env_var() { + use alien_core::bindings::{ + binding_env_var_name, serialize_binding_as_env_var, ExternalAiBinding, + }; + use alien_core::ExternalBinding; + + let external = ExternalBinding::Ai(ExternalAiBinding { + provider: "openai".to_string(), + api_key: "sk-test".into(), + }); + + let value = external + .to_env_binding_value() + .expect("external AI binding should serialize"); + + let env = serialize_binding_as_env_var("llm", &value).unwrap(); + let key = binding_env_var_name("llm"); + assert_eq!(key, "ALIEN_LLM_BINDING"); + + let injected: serde_json::Value = + serde_json::from_str(env.get(&key).expect("binding env var present")).unwrap(); + assert_eq!( + injected, + json!({ + "service": "external", + "provider": "openai", + "apiKey": "sk-test", + }), + "injected AI binding env var must match the SDK's expected service-tagged shape" + ); + } + #[test] fn test_build_without_bindings_returns_empty_list() { let env = HashMap::from([("FOO".to_string(), "bar".to_string())]); diff --git a/crates/alien-infra/src/core/executor_tests/edge_cases_tests.rs b/crates/alien-infra/src/core/executor_tests/edge_cases_tests.rs index d75b4552c..7afe2c0d7 100644 --- a/crates/alien-infra/src/core/executor_tests/edge_cases_tests.rs +++ b/crates/alien-infra/src/core/executor_tests/edge_cases_tests.rs @@ -4,9 +4,9 @@ use super::helpers::*; use crate::core::StackExecutor; use crate::error::Result; use alien_core::{ - ClientConfig, DeploymentConfig, EnvironmentVariablesSnapshot, ExternalBinding, - ExternalBindings, Resource, ResourceLifecycle, ResourceRef, ResourceStatus, Stack, - StackSettings, Storage, StorageBinding, + bindings::ExternalAiBinding, Ai, ClientConfig, DeploymentConfig, EnvironmentVariablesSnapshot, + ExternalBinding, ExternalBindings, Resource, ResourceLifecycle, ResourceRef, ResourceStatus, + Stack, StackSettings, Storage, StorageBinding, }; fn external_storage_config(resource_id: &str) -> DeploymentConfig { @@ -28,6 +28,28 @@ fn external_storage_config(resource_id: &str) -> DeploymentConfig { .build() } +fn external_ai_config(resource_id: &str) -> DeploymentConfig { + let mut external_bindings = ExternalBindings::new(); + external_bindings.insert( + resource_id, + ExternalBinding::Ai(ExternalAiBinding { + provider: "openai".to_string(), + api_key: "sk-test".into(), + }), + ); + + DeploymentConfig::builder() + .stack_settings(StackSettings::default()) + .environment_variables(EnvironmentVariablesSnapshot { + variables: vec![], + hash: String::new(), + created_at: String::new(), + }) + .external_bindings(external_bindings) + .allow_frozen_changes(false) + .build() +} + async fn assert_best_effort_delete_error_marks_deleted(flag: &str) -> Result<()> { let mut func1 = test_function("func1"); func1 @@ -563,6 +585,37 @@ async fn test_external_binding_resource_syncs_without_controller() -> Result<()> Ok(()) } +/// An external (BYO-key) AI resource must be treated as an external binding: +/// the cloud AI controller (Bedrock/Vertex/Foundry) is SKIPPED and the resource +/// syncs Running with no controller state. Without this, an external OpenAI +/// resource would silently provision and route to Bedrock. +#[tokio::test] +async fn test_external_ai_binding_skips_cloud_controller() -> Result<()> { + let ai = Ai::new("llm".to_string()).build(); + let stack = Stack::new("external-ai-sync-test".to_owned()) + .add(ai.clone(), ResourceLifecycle::Frozen) + .build(); + let deployment_config = external_ai_config("llm"); + let executor = StackExecutor::builder(&stack, ClientConfig::Test) + .deployment_config(&deployment_config) + .build()?; + + let state = run_to_synced(&executor, new_test_state()).await?; + let external_state = state.resources.get("llm").unwrap(); + + assert_eq!(external_state.status, ResourceStatus::Running); + assert!( + external_state.internal_state.is_none(), + "External AI binding must not run the cloud controller (no controller state)" + ); + assert!( + executor.is_synced(&state), + "Desired external AI binding should be considered synced while Running" + ); + + Ok(()) +} + /// Full-stack deletion should mark controllerless external bindings deleted /// instead of looping forever waiting for a controller that intentionally /// does not exist. diff --git a/crates/alien-infra/src/core/registry.rs b/crates/alien-infra/src/core/registry.rs index d2942f635..74c2a5dbf 100644 --- a/crates/alien-infra/src/core/registry.rs +++ b/crates/alien-infra/src/core/registry.rs @@ -15,7 +15,7 @@ use alien_core::Daemon; #[cfg(feature = "local")] use alien_core::Postgres; use alien_core::{ - ArtifactRegistry, AwsOpenSearch, AzureContainerAppsEnvironment, AzureResourceGroup, + Ai, ArtifactRegistry, AwsOpenSearch, AzureContainerAppsEnvironment, AzureResourceGroup, AzureServiceBusNamespace, AzureStorageAccount, Build, Email, Kv, Network, RemoteStackManagement, ServiceAccount, ServiceActivation, Storage, Vault, Worker, }; @@ -669,6 +669,32 @@ impl ResourceRegistry { >::new()), ); + // Register built-in Ai controllers + #[cfg(feature = "aws")] + registry.register_controller_factory( + Ai::RESOURCE_TYPE, + Platform::Aws, + Box::new(DefaultControllerFactory::::new()), + ); + #[cfg(feature = "gcp")] + registry.register_controller_factory( + Ai::RESOURCE_TYPE, + Platform::Gcp, + Box::new(DefaultControllerFactory::::new()), + ); + #[cfg(feature = "azure")] + registry.register_controller_factory( + Ai::RESOURCE_TYPE, + Platform::Azure, + Box::new(DefaultControllerFactory::::new()), + ); + #[cfg(feature = "local")] + registry.register_controller_factory( + Ai::RESOURCE_TYPE, + Platform::Local, + Box::new(DefaultControllerFactory::::new()), + ); + // Register Local ComputeCluster controller #[cfg(feature = "local")] registry.register_controller_factory( diff --git a/crates/alien-infra/src/core/service_provider.rs b/crates/alien-infra/src/core/service_provider.rs index 512159c1f..d2c30162e 100644 --- a/crates/alien-infra/src/core/service_provider.rs +++ b/crates/alien-infra/src/core/service_provider.rs @@ -3,6 +3,7 @@ use alien_aws_clients::{ acm::{AcmApi, AcmClient}, apigatewayv2::{ApiGatewayV2Api, ApiGatewayV2Client}, autoscaling::{AutoScalingApi, AutoScalingClient}, + bedrock::{BedrockApi, BedrockClient}, cloudformation::{CloudFormationApi, CloudFormationClient}, codebuild::{CodeBuildApi, CodeBuildClient}, dynamodb::{DynamoDbApi, DynamoDbClient}, @@ -39,6 +40,7 @@ use alien_azure_clients::{ managed_clusters::{AzureManagedClustersClient, ManagedClustersApi}, managed_identity::{AzureManagedIdentityClient, ManagedIdentityApi}, network::{AzureNetworkClient, NetworkApi as AzureNetworkApi}, + openai_finetuning::{AzureFoundryFineTuningClient, FoundryFineTuningApi}, private_networking::{AzurePrivateNetworkingClient, PrivateNetworkingApi}, resource_skus::{AzureResourceSkusClient, ResourceSkusApi}, resources::{AzureResourcesClient, ResourcesApi}, @@ -46,12 +48,14 @@ use alien_azure_clients::{ AzureServiceBusDataPlaneClient, AzureServiceBusManagementClient, ServiceBusDataPlaneApi, ServiceBusManagementApi, }, + cognitive_services::{AzureCognitiveServicesClient, CognitiveServicesAccountsApi}, storage_accounts::{AzureStorageAccountsClient, StorageAccountsApi}, tables::{AzureTableManagementClient, TableManagementApi}, AzureClientConfig, AzureTokenCache, }; use alien_error::Context; use alien_gcp_clients::{ + aiplatform::{AiPlatformApi, AiPlatformClient}, artifactregistry::{ArtifactRegistryApi, ArtifactRegistryClient}, cloud_sql::{CloudSqlApi, CloudSqlClient}, cloudbuild::{CloudBuildApi, CloudBuildClient}, @@ -128,6 +132,10 @@ pub trait PlatformServiceProvider: Send + Sync { &self, config: &AwsClientConfig, ) -> Result>; + async fn get_aws_bedrock_client( + &self, + config: &AwsClientConfig, + ) -> Result>; // GCP clients fn get_gcp_iam_client(&self, config: &GcpClientConfig) -> Result>; @@ -163,6 +171,10 @@ pub trait PlatformServiceProvider: Send + Sync { &self, config: &GcpClientConfig, ) -> Result>; + fn get_gcp_aiplatform_client( + &self, + config: &GcpClientConfig, + ) -> Result>; // Azure clients fn get_azure_application_gateway_client( @@ -225,6 +237,14 @@ pub trait PlatformServiceProvider: Send + Sync { &self, config: &AzureClientConfig, ) -> Result>; + fn get_azure_cognitive_services_client( + &self, + config: &AzureClientConfig, + ) -> Result>; + fn get_azure_foundry_finetuning_client( + &self, + config: &AzureClientConfig, + ) -> Result>; fn get_azure_key_vault_management_client( &self, config: &AzureClientConfig, @@ -677,6 +697,22 @@ impl PlatformServiceProvider for DefaultPlatformServiceProvider { ))) } + async fn get_aws_bedrock_client( + &self, + config: &AwsClientConfig, + ) -> Result> { + let credentials = AwsCredentialProvider::from_config(config.clone()) + .await + .context(crate::error::ErrorData::CloudPlatformError { + message: "Failed to create AWS credential provider".to_string(), + resource_id: None, + })?; + Ok(Arc::new(BedrockClient::new( + reqwest::Client::new(), + credentials, + ))) + } + // GCP implementations fn get_gcp_iam_client(&self, config: &GcpClientConfig) -> Result> { Ok(Arc::new(GcpIamClient::new( @@ -797,6 +833,16 @@ impl PlatformServiceProvider for DefaultPlatformServiceProvider { ))) } + fn get_gcp_aiplatform_client( + &self, + config: &GcpClientConfig, + ) -> Result> { + Ok(Arc::new(AiPlatformClient::new( + reqwest::Client::new(), + config.clone(), + ))) + } + // Azure implementations fn get_azure_authorization_client( &self, @@ -948,6 +994,26 @@ impl PlatformServiceProvider for DefaultPlatformServiceProvider { ))) } + fn get_azure_cognitive_services_client( + &self, + config: &AzureClientConfig, + ) -> Result> { + Ok(Arc::new(AzureCognitiveServicesClient::new( + reqwest::Client::new(), + AzureTokenCache::new(config.clone()), + ))) + } + + fn get_azure_foundry_finetuning_client( + &self, + config: &AzureClientConfig, + ) -> Result> { + Ok(Arc::new(AzureFoundryFineTuningClient::new( + reqwest::Client::new(), + AzureTokenCache::new(config.clone()), + ))) + } + fn get_azure_key_vault_management_client( &self, config: &AzureClientConfig, diff --git a/crates/alien-infra/src/daemon/local.rs b/crates/alien-infra/src/daemon/local.rs index 071380212..da37cb104 100644 --- a/crates/alien-infra/src/daemon/local.rs +++ b/crates/alien-infra/src/daemon/local.rs @@ -12,7 +12,7 @@ use crate::error::{ErrorData, Result}; use alien_core::{ Daemon, DaemonCode, DaemonHeartbeatData, DaemonOutputs, HeartbeatBackend, LocalDaemonHeartbeatData, LocalRuntimeUnitKind, LocalRuntimeUnitStatus, ObservedHealth, - Platform, Postgres, ProviderLifecycleState, ResourceHeartbeat, ResourceHeartbeatData, + Platform, ProviderLifecycleState, ResourceHeartbeat, ResourceHeartbeatData, ResourceOutputs, ResourceStatus, WorkloadHeartbeatStatus, }; use alien_error::{AlienError, Context}; @@ -159,21 +159,16 @@ impl LocalDaemonController { runtime_only_env_names.sort(); runtime_only_env_names.dedup(); - // Linked Postgres resources carry a runtime-only secret (the password). Name them so the - // worker manager delivers the binding to the process but never persists it to metadata. - let runtime_only_binding_names: Vec = config - .links - .iter() - .filter(|link| link.resource_type() == &Postgres::RESOURCE_TYPE) - .map(|link| link.id().to_string()) - .collect(); + // Links whose binding carries a runtime-only secret (Postgres password, BYO-key AI): the + // supervisor delivers the binding to the process but never persists it to metadata. + let runtime_only_bindings = alien_local::RuntimeOnlyBindingRef::from_links(&config.links); manager .start_daemon( &config.id, env_vars, alien_local::DaemonLaunchOptions { - runtime_only_binding_names, + runtime_only_bindings, runtime_only_env_names, command_override: config.command.clone(), stop_grace_period_seconds: config.stop_grace_period_seconds, diff --git a/crates/alien-infra/src/gcp_importers.rs b/crates/alien-infra/src/gcp_importers.rs index 53fc70826..3723345e4 100644 --- a/crates/alien-infra/src/gcp_importers.rs +++ b/crates/alien-infra/src/gcp_importers.rs @@ -5,9 +5,12 @@ #[cfg(feature = "kubernetes")] use alien_core::KubernetesCluster; -use alien_core::{ArtifactRegistry, Build, Kv, Network, Platform, Queue, Storage, Vault, Worker}; +use alien_core::{ + Ai, ArtifactRegistry, Build, Kv, Network, Platform, Queue, Storage, Vault, Worker, +}; use alien_core::{RemoteStackManagement, ServiceAccount, ServiceActivation}; +use crate::ai::GcpAiImporter; use crate::artifact_registry::GcpArtifactRegistryImporter; use crate::build::GcpBuildImporter; #[cfg(feature = "kubernetes")] @@ -26,6 +29,7 @@ use crate::ImporterRegistry; /// Register every OSS GCP importer with `registry`. pub fn register(registry: &mut ImporterRegistry) { registry + .register(Ai::RESOURCE_TYPE, Platform::Gcp, GcpAiImporter) .register(Storage::RESOURCE_TYPE, Platform::Gcp, GcpStorageImporter) .register(Kv::RESOURCE_TYPE, Platform::Gcp, GcpKvImporter) .register(Vault::RESOURCE_TYPE, Platform::Gcp, GcpVaultImporter) diff --git a/crates/alien-infra/src/lib.rs b/crates/alien-infra/src/lib.rs index b6ead7152..14798bb21 100644 --- a/crates/alien-infra/src/lib.rs +++ b/crates/alien-infra/src/lib.rs @@ -59,6 +59,19 @@ mod container; #[cfg(any(feature = "kubernetes", feature = "local"))] pub use container::*; +mod ai; +pub use ai::AwsAiController; +#[cfg(feature = "aws")] +pub use ai::AwsAiImporter; +#[cfg(feature = "gcp")] +pub use ai::GcpAiController; +#[cfg(feature = "gcp")] +pub use ai::GcpAiImporter; +#[cfg(feature = "azure")] +pub use ai::AzureAiController; +#[cfg(feature = "azure")] +pub use ai::AzureAiImporter; + mod kv; #[cfg(feature = "aws")] pub use kv::AwsKvImporter; diff --git a/crates/alien-infra/src/worker/local.rs b/crates/alien-infra/src/worker/local.rs index 580a778fe..89dc36a40 100644 --- a/crates/alien-infra/src/worker/local.rs +++ b/crates/alien-infra/src/worker/local.rs @@ -9,7 +9,7 @@ use crate::core::{ use crate::error::{ErrorData, Result}; use alien_core::{ HeartbeatBackend, LocalRuntimeUnitKind, LocalRuntimeUnitStatus, LocalWorkerHeartbeatData, - ObservedHealth, Platform, Postgres, ProviderLifecycleState, ResourceHeartbeat, + ObservedHealth, Platform, ProviderLifecycleState, ResourceHeartbeat, ResourceHeartbeatData, ResourceOutputs as CoreResourceOutputs, ResourceStatus, Worker, WorkerCode, WorkerHeartbeatData, WorkerOutputs, WorkloadHeartbeatStatus, ENV_ALIEN_COMMANDS_TOKEN, @@ -157,21 +157,16 @@ impl LocalWorkerController { runtime_only_env_names.sort(); env_vars.extend(runtime_control_env); - // Linked Postgres resources carry a runtime-only secret (the password). Name them so the - // worker manager delivers the binding to the process but never persists it to metadata. - let runtime_only_binding_names: Vec = config - .links - .iter() - .filter(|link| link.resource_type() == &Postgres::RESOURCE_TYPE) - .map(|link| link.id().to_string()) - .collect(); + // Links whose binding carries a runtime-only secret (Postgres password, BYO-key AI): + // the worker manager delivers the binding to the process but never persists it to metadata. + let runtime_only_bindings = alien_local::RuntimeOnlyBindingRef::from_links(&config.links); // Start the worker with complete environment let worker_url = func_mgr .start_worker( &config.id, env_vars, - runtime_only_binding_names, + runtime_only_bindings, runtime_only_env_names, ) .await diff --git a/crates/alien-infra/tests/importers.rs b/crates/alien-infra/tests/importers.rs index 85df2ca6e..b2e4610f5 100644 --- a/crates/alien-infra/tests/importers.rs +++ b/crates/alien-infra/tests/importers.rs @@ -24,17 +24,18 @@ use alien_core::import::{ data::{ - AwsKvImportData, AwsRemoteStackManagementImportData, AwsServiceAccountImportData, - AwsStorageImportData, AzureContainerAppsEnvironmentImportData, + AwsAiImportData, AwsKvImportData, AwsRemoteStackManagementImportData, AwsServiceAccountImportData, + AwsStorageImportData, AzureAiImportData, + AzureContainerAppsEnvironmentImportData, AzureRemoteStackManagementImportData, AzureResourceGroupImportData, AzureServiceAccountImportData, AzureStorageAccountImportData, AzureStorageImportData, - GcpBuildImportData, GcpKvImportData, GcpNetworkImportData, GcpServiceActivationImportData, + GcpAiImportData, GcpBuildImportData, GcpKvImportData, GcpNetworkImportData, GcpServiceActivationImportData, GcpStorageImportData, KubernetesClusterImportData, }, ImportContext, }; use alien_core::{ - ArtifactRegistry, AwsManagementConfig, AwsOpenSearch, AwsOpenSearchOutputs, + Ai, ArtifactRegistry, AwsManagementConfig, AwsOpenSearch, AwsOpenSearchOutputs, AzureContainerAppsEnvironment, AzureContainerAppsEnvironmentOutputs, AzureManagementConfig, AzureResourceGroup, AzureResourceGroupOutputs, AzureServiceBusNamespace, AzureStorageAccount, AzureStorageAccountOutputs, Build, Email, EmailInbound, EmailOutputs, GcpManagementConfig, @@ -864,11 +865,139 @@ fn azure_remote_stack_management_round_trip_includes_access_outputs() { ); } +#[test] +fn aws_ai_round_trip() { + let entry = entry(Ai::new("llm".to_string()).build()); + let data = AwsAiImportData { + region: "us-east-1".to_string(), + }; + let state = run_through_registry( + &Ai::RESOURCE_TYPE, + Platform::Aws, + serde_json::to_value(&data).unwrap(), + &entry, + "us-east-1", + &aws_management_config(), + ); + assert_running_with_internal_state(&state); + + // binding params must carry the region and identify Bedrock + let binding = state + .remote_binding_params + .as_ref() + .expect("AWS AI import must produce binding params"); + assert_eq!(binding["service"], "bedrock"); + assert_eq!(binding["region"], "us-east-1"); + + // outputs must expose provider "bedrock" + let outputs = state + .outputs + .as_ref() + .and_then(|o| o.downcast_ref::()) + .expect("AWS AI import must produce AiOutputs"); + assert_eq!(outputs.provider, "bedrock"); + assert!( + outputs + .endpoint + .as_ref() + .is_some_and(|ep| ep.contains("us-east-1")), + "endpoint must contain the region" + ); +} + +#[test] +fn gcp_ai_round_trip() { + let entry = entry(Ai::new("llm".to_string()).build()); + let data = GcpAiImportData { + project_id: "my-project".to_string(), + location: "us-central1".to_string(), + }; + let state = run_through_registry( + &Ai::RESOURCE_TYPE, + Platform::Gcp, + serde_json::to_value(&data).unwrap(), + &entry, + "us-central1", + &gcp_management_config(), + ); + assert_running_with_internal_state(&state); + + // binding params must carry project + location and identify Vertex AI + let binding = state + .remote_binding_params + .as_ref() + .expect("GCP AI import must produce binding params"); + assert_eq!(binding["service"], "vertex"); + assert_eq!(binding["project"], "my-project"); + assert_eq!(binding["location"], "us-central1"); + + // outputs must expose provider "vertex" + let outputs = state + .outputs + .as_ref() + .and_then(|o| o.downcast_ref::()) + .expect("GCP AI import must produce AiOutputs"); + assert_eq!(outputs.provider, "vertex"); + assert!( + outputs + .endpoint + .as_ref() + .is_some_and(|ep| ep.contains("us-central1") && ep.contains("my-project")), + "endpoint must contain the location and project" + ); +} + +#[test] +fn azure_ai_round_trip() { + let entry = entry(Ai::new("llm".to_string()).build()); + let data = AzureAiImportData { + account_name: "myprefix-llm".to_string(), + endpoint: "https://myprefix-llm.cognitiveservices.azure.com/".to_string(), + resource_group: "rg-alien".to_string(), + location: "eastus".to_string(), + }; + let state = run_through_registry( + &Ai::RESOURCE_TYPE, + Platform::Azure, + serde_json::to_value(&data).unwrap(), + &entry, + "eastus", + &azure_management_config(), + ); + assert_running_with_internal_state(&state); + + // binding params must carry the endpoint + account and identify Foundry + let binding = state + .remote_binding_params + .as_ref() + .expect("Azure AI import must produce binding params"); + assert_eq!(binding["service"], "foundry"); + assert_eq!( + binding["endpoint"], + "https://myprefix-llm.cognitiveservices.azure.com/" + ); + assert_eq!(binding["account"], "myprefix-llm"); + + // outputs must expose provider "foundry" + let outputs = state + .outputs + .as_ref() + .and_then(|o| o.downcast_ref::()) + .expect("Azure AI import must produce AiOutputs"); + assert_eq!(outputs.provider, "foundry"); + assert_eq!( + outputs.endpoint.as_deref(), + Some("https://myprefix-llm.cognitiveservices.azure.com/") + ); + assert_eq!(outputs.account.as_deref(), Some("myprefix-llm")); +} + #[test] fn registry_built_in_covers_all_oss_pairs() { let registry = ImporterRegistry::built_in(); let aws_pairs: &[ResourceType] = &[ + Ai::RESOURCE_TYPE, Storage::RESOURCE_TYPE, Kv::RESOURCE_TYPE, Vault::RESOURCE_TYPE, @@ -891,6 +1020,7 @@ fn registry_built_in_covers_all_oss_pairs() { } let gcp_pairs: &[ResourceType] = &[ + Ai::RESOURCE_TYPE, Storage::RESOURCE_TYPE, Kv::RESOURCE_TYPE, Vault::RESOURCE_TYPE, @@ -912,6 +1042,7 @@ fn registry_built_in_covers_all_oss_pairs() { } let azure_pairs: &[ResourceType] = &[ + Ai::RESOURCE_TYPE, Storage::RESOURCE_TYPE, Kv::RESOURCE_TYPE, Vault::RESOURCE_TYPE, diff --git a/crates/alien-local/src/daemon_supervisor.rs b/crates/alien-local/src/daemon_supervisor.rs index 087fd2421..195797136 100644 --- a/crates/alien-local/src/daemon_supervisor.rs +++ b/crates/alien-local/src/daemon_supervisor.rs @@ -7,7 +7,7 @@ //! worker runtime that [`crate::worker_manager`] hosts for the Worker path. use crate::error::{ErrorData, Result}; -use crate::worker_manager::{LocalWorkerManager, WorkerMetadata}; +use crate::worker_manager::{LocalWorkerManager, RuntimeOnlyBindingRef, WorkerMetadata}; use alien_error::{AlienError, Context, ContextError as _, IntoAlienError}; use alien_worker_runtime::{LogExporter, OwnedOtlpLogger}; use std::collections::HashMap; @@ -48,9 +48,10 @@ pub(crate) struct DaemonRuntime { /// shape without the caller re-supplying it. #[derive(Debug, Default, Clone)] pub struct DaemonLaunchOptions { - /// Linked-resource names whose binding is a runtime-only secret (a local - /// Postgres password): re-resolved live at every start, never persisted. - pub runtime_only_binding_names: Vec, + /// Linked resources whose binding is a runtime-only secret (a local + /// Postgres password or a local BYO-key AI binding): re-resolved live at + /// every start by resource type, never persisted. + pub runtime_only_bindings: Vec, /// Env var NAMES whose resolved values are deployment secrets (including /// the receiver's `ALIEN_COMMANDS_TOKEN`): delivered to the process but /// stripped from the persisted metadata. The in-memory runtime keeps the @@ -166,15 +167,15 @@ impl LocalWorkerManager { // Re-resolve each secret live (kept out of persisted metadata; see plan_worker_launch). let mut resolved_bindings = Vec::new(); - for name in &options.runtime_only_binding_names { + for binding in &options.runtime_only_bindings { if let Some(entry) = bindings_provider - .resolve_runtime_only_binding_env(name) + .resolve_runtime_only_binding_env(&binding.name, &binding.resource_type) .await .context(ErrorData::Other { - message: format!("Failed to resolve runtime-only binding '{}'", name), + message: format!("Failed to resolve runtime-only binding '{}'", binding.name), })? { - resolved_bindings.push((name.clone(), entry)); + resolved_bindings.push((binding.name.clone(), entry)); } } let (updated_metadata, runtime_env_vars) = Self::plan_worker_launch( @@ -183,7 +184,7 @@ impl LocalWorkerManager { &existing_metadata, None, env_vars, - options.runtime_only_binding_names, + options.runtime_only_bindings, &existing_metadata.runtime_only_env_names.clone(), &resolved_bindings, ); diff --git a/crates/alien-local/src/lib.rs b/crates/alien-local/src/lib.rs index 7d8d85bb1..45206a96b 100644 --- a/crates/alien-local/src/lib.rs +++ b/crates/alien-local/src/lib.rs @@ -81,4 +81,4 @@ pub use postgres_manager::LocalPostgresManager; pub use queue_manager::LocalQueueManager; pub use storage_manager::LocalStorageManager; pub use vault_manager::LocalVaultManager; -pub use worker_manager::LocalWorkerManager; +pub use worker_manager::{LocalWorkerManager, RuntimeOnlyBindingRef}; diff --git a/crates/alien-local/src/local_bindings_provider.rs b/crates/alien-local/src/local_bindings_provider.rs index 2832f1067..3d776e54f 100644 --- a/crates/alien-local/src/local_bindings_provider.rs +++ b/crates/alien-local/src/local_bindings_provider.rs @@ -275,33 +275,84 @@ impl LocalBindingsProvider { } } } + + /// Inherent counterpart of [`BindingsProviderApi::resolve_runtime_only_binding_env`], so + /// controllers holding the concrete provider (the container path) can resolve without + /// importing the trait. The resource type routes to the local secret source. + pub async fn resolve_runtime_only_binding_env( + &self, + binding_name: &str, + resource_type: &str, + ) -> alien_bindings::error::Result>> { + if resource_type == alien_core::Postgres::RESOURCE_TYPE.0 { + // Re-reads the password live from the manager's 0600 metadata. `try_get_binding` + // is `None` for a name absent on recover; external (Remote Access) Postgres is + // rejected upstream on the local platform, so it never reaches here. + return match self.postgres_manager.try_get_binding(binding_name).context( + BindingsErrorData::config_invalid( + binding_name, + "Failed to read the local Postgres metadata", + ), + )? { + Some(binding) => Ok(Some( + alien_core::bindings::serialize_binding_as_env_var(binding_name, &binding) + .context(BindingsErrorData::config_invalid( + binding_name, + "Failed to serialize local Postgres binding", + ))?, + )), + None => Ok(None), + }; + } + if resource_type == alien_core::Ai::RESOURCE_TYPE.0 { + let api_key = std::env::var(alien_core::bindings::AiBinding::LOCAL_API_KEY_ENV) + .ok() + .filter(|key| !key.is_empty()) + .ok_or_else(|| { + AlienError::new(BindingsErrorData::config_invalid( + binding_name, + format!( + "local AI binding needs {} set in the environment", + alien_core::bindings::AiBinding::LOCAL_API_KEY_ENV + ), + )) + })?; + return Ok(Some(local_ai_binding_env(binding_name, api_key)?)); + } + // Only types a controller marked runtime-only reach this resolver; anything else is a + // marking/resolution mismatch, not a missing resource. + Err(AlienError::new(BindingsErrorData::config_invalid( + binding_name, + format!("no local runtime-only secret source for resource type '{resource_type}'"), + ))) + } +} + +/// Builds the full BYO-key AI binding env entry for a linked workload. Pure so the +/// key handling is unit-testable; the caller reads the key from the environment. +fn local_ai_binding_env( + binding_name: &str, + api_key: String, +) -> alien_bindings::error::Result> { + let binding = alien_core::bindings::AiBinding::external( + alien_core::bindings::AiBinding::LOCAL_DEFAULT_PROVIDER, + api_key, + ); + alien_core::bindings::serialize_binding_as_env_var(binding_name, &binding).context( + BindingsErrorData::config_invalid(binding_name, "Failed to serialize local AI binding"), + ) } #[async_trait] impl BindingsProviderApi for LocalBindingsProvider { - /// `try_get_binding` is `None` for any name that isn't a managed local Postgres; the password is - /// read live from the manager's 0600 metadata each call. (External/Remote-Access Postgres is - /// rejected upstream on the local platform, so it never reaches here.) async fn resolve_runtime_only_binding_env( &self, binding_name: &str, + resource_type: &str, ) -> alien_bindings::error::Result>> { - match self - .postgres_manager - .try_get_binding(binding_name) - .context(BindingsErrorData::config_invalid( - binding_name, - "Failed to read the local Postgres metadata", - ))? { - Some(binding) => Ok(Some( - alien_core::bindings::serialize_binding_as_env_var(binding_name, &binding) - .context(BindingsErrorData::config_invalid( - binding_name, - "Failed to serialize local Postgres binding", - ))?, - )), - None => Ok(None), - } + // The inherent method shadows this trait method on the concrete type. + LocalBindingsProvider::resolve_runtime_only_binding_env(self, binding_name, resource_type) + .await } async fn load_storage( @@ -602,3 +653,30 @@ impl BindingsProviderApi for LocalBindingsProvider { })) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn local_ai_binding_env_carries_the_full_external_binding() { + let env = local_ai_binding_env("llm", "sk-test-key".to_string()) + .expect("AI binding env should serialize"); + let raw = env + .get("ALIEN_LLM_BINDING") + .expect("binding env var should be present"); + let value: serde_json::Value = serde_json::from_str(raw).expect("binding is JSON"); + assert_eq!( + value.get("service").and_then(|v| v.as_str()), + Some("external") + ); + assert_eq!( + value.get("provider").and_then(|v| v.as_str()), + Some("openai") + ); + assert_eq!( + value.get("apiKey").and_then(|v| v.as_str()), + Some("sk-test-key") + ); + } +} diff --git a/crates/alien-local/src/worker_manager.rs b/crates/alien-local/src/worker_manager.rs index 0841c75fe..50977bdcd 100644 --- a/crates/alien-local/src/worker_manager.rs +++ b/crates/alien-local/src/worker_manager.rs @@ -58,6 +58,36 @@ struct WorkerRuntime { metadata: WorkerMetadata, } +/// A linked resource whose binding carries a runtime-only secret (a local Postgres password, a +/// local BYO-key AI binding). The name locates the binding env var; the resource type routes live +/// re-resolution to the right local source. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RuntimeOnlyBindingRef { + /// The linked resource's binding name (`ALIEN__BINDING`). + pub name: String, + /// The linked resource's type (e.g. "postgres", "ai"). + pub resource_type: String, +} + +impl RuntimeOnlyBindingRef { + /// Refs for the links whose Local binding carries a runtime-only secret — the single list of + /// types `LocalBindingsProvider::resolve_runtime_only_binding_env` knows how to re-resolve. + pub fn from_links(links: &[alien_core::ResourceRef]) -> Vec { + links + .iter() + .filter(|link| { + link.resource_type() == &alien_core::Postgres::RESOURCE_TYPE + || link.resource_type() == &alien_core::Ai::RESOURCE_TYPE + }) + .map(|link| Self { + name: link.id().to_string(), + resource_type: link.resource_type().to_string(), + }) + .collect() + } +} + /// Persistent metadata for a worker (saved to disk) #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct WorkerMetadata { @@ -74,9 +104,13 @@ pub(crate) struct WorkerMetadata { /// Transport port for the runtime (persisted to enable transparent recovery) #[serde(default)] pub(crate) transport_port: Option, - /// Names of linked resources whose binding is a runtime-only secret (a local Postgres password), - /// persisted so recovery/restart can re-resolve it live; the secret itself is never written here. + /// Linked resources whose binding is a runtime-only secret, persisted so recovery/restart + /// can re-resolve it live by resource type; the secret itself is never written here. #[serde(default)] + pub(crate) runtime_only_bindings: Vec, + /// Names-only form written by older CLIs (no resource type recorded); folded into + /// `runtime_only_bindings` on read, never written back. + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub(crate) runtime_only_binding_names: Vec, /// Env var NAMES whose resolved values are deployment secrets; the values /// are stripped from this persisted file (see `plan_worker_launch`) and @@ -90,6 +124,23 @@ pub(crate) struct WorkerMetadata { pub(crate) stop_grace_period_seconds: Option, } +impl WorkerMetadata { + /// Typed runtime-only refs for (re)start, folding legacy names-only entries in as Postgres — + /// the only type that existed when the names-only format was written. + fn runtime_only_binding_refs(&self) -> Vec { + let mut refs = self.runtime_only_bindings.clone(); + for name in &self.runtime_only_binding_names { + if !refs.iter().any(|r| &r.name == name) { + refs.push(RuntimeOnlyBindingRef { + name: name.clone(), + resource_type: alien_core::Postgres::RESOURCE_TYPE.to_string(), + }); + } + } + refs + } +} + impl LocalWorkerManager { /// Creates a new worker manager with shared shutdown signal. /// @@ -261,10 +312,11 @@ impl LocalWorkerManager { info!(worker_id = %metadata.worker_id, "Recovering worker from previous run"); // Restart the worker using metadata + let runtime_only_bindings = metadata.runtime_only_binding_refs(); Self::start_worker_internal( &metadata.worker_id, metadata.env_vars, - metadata.runtime_only_binding_names, + runtime_only_bindings, metadata.runtime_only_env_names, state_dir, workers, @@ -330,10 +382,11 @@ impl LocalWorkerManager { warn!(worker_id = %worker_id, "Auto-restarting worker..."); // Restart using metadata + let runtime_only_bindings = metadata.runtime_only_binding_refs(); if let Err(e) = Self::start_worker_internal( &metadata.worker_id, metadata.env_vars, - metadata.runtime_only_binding_names, + runtime_only_bindings, metadata.runtime_only_env_names, state_dir, workers, @@ -486,13 +539,13 @@ impl LocalWorkerManager { &self, id: &str, env_vars: HashMap, - runtime_only_binding_names: Vec, + runtime_only_bindings: Vec, runtime_only_env_names: Vec, ) -> Result { Self::start_worker_internal( id, env_vars, - runtime_only_binding_names, + runtime_only_bindings, runtime_only_env_names, &self.state_dir, &self.workers, @@ -502,7 +555,7 @@ impl LocalWorkerManager { } /// Builds the worker's persisted metadata and its live process env. The persisted metadata has - /// each named runtime-only binding's key removed — keyed on the names, not on what re-resolved, + /// each marked runtime-only binding's key removed — keyed on the refs, not on what re-resolved, /// so a secret never persists even if live re-resolution returns nothing (e.g. the resource /// vanished between env-build and start) — while the live env keeps the re-resolved secret. Pure /// (no IO) so the "password never persisted" invariant is unit-testable on the artifact we @@ -514,7 +567,7 @@ impl LocalWorkerManager { existing: &WorkerMetadata, transport_port: Option, passed_env: HashMap, - runtime_only_binding_names: Vec, + runtime_only_bindings: Vec, runtime_only_env_names: &[String], resolved: &[(String, HashMap)], ) -> (WorkerMetadata, HashMap) { @@ -528,8 +581,8 @@ impl LocalWorkerManager { { runtime_only_env_names.push(alien_core::ENV_ALIEN_COMMANDS_TOKEN.to_string()); } - for name in &runtime_only_binding_names { - persisted_env.remove(&alien_core::bindings::binding_env_var_name(name)); + for binding in &runtime_only_bindings { + persisted_env.remove(&alien_core::bindings::binding_env_var_name(&binding.name)); } // Resolved deployment secrets (including ALIEN_COMMANDS_TOKEN) never // persist either — same invariant as the binding secrets above. @@ -546,7 +599,8 @@ impl LocalWorkerManager { runtime_command: existing.runtime_command.clone(), working_dir: existing.working_dir.clone(), transport_port, - runtime_only_binding_names, + runtime_only_bindings, + runtime_only_binding_names: Vec::new(), runtime_only_env_names, stop_grace_period_seconds: existing.stop_grace_period_seconds, }; @@ -557,7 +611,7 @@ impl LocalWorkerManager { async fn start_worker_internal( id: &str, env_vars: HashMap, - runtime_only_binding_names: Vec, + runtime_only_bindings: Vec, runtime_only_env_names: Vec, state_dir: &PathBuf, workers: &Arc>>, @@ -627,15 +681,15 @@ impl LocalWorkerManager { // Re-resolve each secret live (kept out of persisted metadata; see plan_worker_launch). let mut resolved_bindings = Vec::new(); - for name in &runtime_only_binding_names { + for binding in &runtime_only_bindings { if let Some(entry) = bindings_provider - .resolve_runtime_only_binding_env(name) + .resolve_runtime_only_binding_env(&binding.name, &binding.resource_type) .await .context(ErrorData::Other { - message: format!("Failed to resolve runtime-only binding '{}'", name), + message: format!("Failed to resolve runtime-only binding '{}'", binding.name), })? { - resolved_bindings.push((name.clone(), entry)); + resolved_bindings.push((binding.name.clone(), entry)); } } let (updated_metadata, runtime_env_vars) = Self::plan_worker_launch( @@ -644,7 +698,7 @@ impl LocalWorkerManager { &existing_metadata, Some(port), env_vars, - runtime_only_binding_names, + runtime_only_bindings, &runtime_only_env_names, &resolved_bindings, ); @@ -1285,7 +1339,8 @@ impl LocalWorkerManager { runtime_command: metadata.runtime_command(), working_dir: metadata.working_dir, transport_port: None, // Will be allocated during start_worker - runtime_only_binding_names: Vec::new(), // Will be set during start_worker + runtime_only_bindings: Vec::new(), // Will be set during start_worker + runtime_only_binding_names: Vec::new(), runtime_only_env_names: Vec::new(), // Will be set during start_daemon stop_grace_period_seconds: None, // Will be set during start_daemon }; @@ -1362,7 +1417,8 @@ impl LocalWorkerManager { runtime_command: metadata.runtime_command(), working_dir: metadata.working_dir, transport_port: None, // Will be allocated during start_worker - runtime_only_binding_names: Vec::new(), // Will be set during start_worker + runtime_only_bindings: Vec::new(), // Will be set during start_worker + runtime_only_binding_names: Vec::new(), runtime_only_env_names: Vec::new(), // Will be set during start_daemon stop_grace_period_seconds: None, // Will be set during start_daemon }; @@ -1506,6 +1562,7 @@ mod tests { runtime_command: vec!["bun".to_string()], working_dir: None, transport_port: None, + runtime_only_bindings: Vec::new(), runtime_only_binding_names: Vec::new(), runtime_only_env_names: Vec::new(), stop_grace_period_seconds: None, @@ -1530,21 +1587,22 @@ mod tests { &existing, Some(3000), base, - vec!["pgdb".to_string()], + vec![RuntimeOnlyBindingRef { + name: "pgdb".to_string(), + resource_type: "postgres".to_string(), + }], &[], &resolved, ); // Persisted metadata: no password, no binding key; the (non-secret) link name stays. let json = serde_json::to_string(&metadata).expect("metadata serializes"); - assert!( - !json.contains("s3cr3t"), - "persisted metadata leaks the password: {json}" - ); + assert!(!json.contains("s3cr3t"), "persisted metadata leaks the password: {json}"); assert!(!metadata.env_vars.contains_key("ALIEN_PGDB_BINDING")); assert!(metadata - .runtime_only_binding_names - .contains(&"pgdb".to_string())); + .runtime_only_bindings + .iter() + .any(|r| r.name == "pgdb" && r.resource_type == "postgres")); // Live process env: the password is delivered to the worker. assert!(live @@ -1562,6 +1620,7 @@ mod tests { runtime_command: vec!["bun".to_string()], working_dir: None, transport_port: None, + runtime_only_bindings: Vec::new(), runtime_only_binding_names: Vec::new(), runtime_only_env_names: Vec::new(), stop_grace_period_seconds: None, @@ -1616,6 +1675,7 @@ mod tests { runtime_command: Vec::new(), working_dir: None, transport_port: None, + runtime_only_bindings: Vec::new(), runtime_only_binding_names: vec!["database".to_string()], runtime_only_env_names: Vec::new(), stop_grace_period_seconds: None, @@ -1644,6 +1704,7 @@ mod tests { runtime_command: Vec::new(), working_dir: None, transport_port: None, + runtime_only_bindings: Vec::new(), runtime_only_binding_names: Vec::new(), runtime_only_env_names: Vec::new(), stop_grace_period_seconds: None, @@ -1676,6 +1737,7 @@ mod tests { runtime_command: Vec::new(), working_dir: None, transport_port: None, + runtime_only_bindings: Vec::new(), runtime_only_binding_names: Vec::new(), runtime_only_env_names: Vec::new(), stop_grace_period_seconds: None, @@ -1693,19 +1755,44 @@ mod tests { &existing, Some(3000), base, - vec!["pgdb".to_string()], + vec![RuntimeOnlyBindingRef { + name: "pgdb".to_string(), + resource_type: "postgres".to_string(), + }], &[], &[], ); let json = serde_json::to_string(&metadata).expect("metadata serializes"); - assert!( - !json.contains("s3cr3t"), - "named binding must be stripped even unresolved: {json}" - ); + assert!(!json.contains("s3cr3t"), "named binding must be stripped even unresolved: {json}"); assert!(!metadata.env_vars.contains_key("ALIEN_PGDB_BINDING")); assert_eq!(metadata.env_vars.get("FOO"), Some(&"bar".to_string())); } + /// Metadata written by a names-only CLI (the Postgres-only era) still recovers: the legacy + /// names fold into typed refs as Postgres, and typed entries win over a same-name legacy one. + #[test] + fn legacy_names_only_metadata_recovers_as_postgres_refs() { + let metadata: WorkerMetadata = serde_json::from_str( + r#"{ + "worker_id": "w", + "extracted_path": "/w", + "env_vars": {}, + "runtime_command": [], + "working_dir": null, + "runtime_only_binding_names": ["db"] + }"#, + ) + .expect("legacy metadata deserializes"); + let refs = metadata.runtime_only_binding_refs(); + assert_eq!( + refs, + vec![RuntimeOnlyBindingRef { + name: "db".to_string(), + resource_type: "postgres".to_string(), + }] + ); + } + fn paths(names: &[&str]) -> Vec { names .iter() diff --git a/crates/alien-permissions/permission-sets/ai/finetune.jsonc b/crates/alien-permissions/permission-sets/ai/finetune.jsonc new file mode 100644 index 000000000..0909629e4 --- /dev/null +++ b/crates/alien-permissions/permission-sets/ai/finetune.jsonc @@ -0,0 +1,90 @@ +{ + "id": "ai/finetune", + "description": "Allows submitting and monitoring model fine-tuning jobs and reading the training dataset from the customer's object storage", + "platforms": { + "aws": [ + { + // Bedrock model customization is a control-plane job the workload submits, + // polls, and (for on-demand serving) invokes the resulting custom model. + // The job reads its dataset from and writes metrics to a customer S3 + // bucket; those S3 grants are scoped by the storage resource's own + // permission set, so this set only needs the Bedrock job + custom-model + // actions. The custom-model ARN is not known until the job completes, so + // the resource ARN is the account-wide custom-model / job namespace. + "grant": { + "actions": [ + "bedrock:CreateModelCustomizationJob", + "bedrock:GetModelCustomizationJob", + "bedrock:StopModelCustomizationJob", + "bedrock:GetCustomModel", + "bedrock:ListCustomModels", + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ] + }, + "binding": { + "stack": { + "resources": [ + "arn:aws:bedrock:*::foundation-model/*", + "arn:aws:bedrock:*:${awsAccountId}:custom-model/*", + "arn:aws:bedrock:*:${awsAccountId}:model-customization-job/*" + ] + }, + "resource": { + "resources": [ + "arn:aws:bedrock:*::foundation-model/*", + "arn:aws:bedrock:*:${awsAccountId}:custom-model/*", + "arn:aws:bedrock:*:${awsAccountId}:model-customization-job/*" + ] + } + } + } + ], + "gcp": [ + { + // Vertex tuning jobs run under a custom role. roles/aiplatform.user carries + // far more than tuning needs; a custom role with the tuning-job lifecycle + // and the predict permissions for serving the tuned endpoint is the + // least-privilege set. Reading the dataset from GCS is granted by the + // storage resource's own set, not here. + "grant": { + "permissions": [ + "aiplatform.tuningJobs.create", + "aiplatform.tuningJobs.get", + "aiplatform.tuningJobs.list", + "aiplatform.tuningJobs.cancel", + "aiplatform.models.get", + "aiplatform.endpoints.predict" + ] + }, + "binding": { + "stack": { "scope": "projects/${projectName}" }, + "resource": { "scope": "projects/${projectName}" } + } + } + ], + "azure": [ + { + // Foundry fine-tuning + deploying the tuned model are account-scoped + // control-plane operations. "Cognitive Services Contributor" is the + // least-privilege built-in role that can create fine-tuning jobs and + // model deployments on the account; "Cognitive Services OpenAI User" + // is added so the same workload can invoke the tuned deployment. + "grant": { + "predefinedRoles": [ + "Cognitive Services Contributor", + "Cognitive Services OpenAI User" + ] + }, + "binding": { + "stack": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}" + }, + "resource": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.CognitiveServices/accounts/${resourceName}" + } + } + } + ] + } +} diff --git a/crates/alien-permissions/permission-sets/ai/heartbeat.jsonc b/crates/alien-permissions/permission-sets/ai/heartbeat.jsonc new file mode 100644 index 000000000..f4a1bc7b1 --- /dev/null +++ b/crates/alien-permissions/permission-sets/ai/heartbeat.jsonc @@ -0,0 +1,53 @@ +{ + "id": "ai/heartbeat", + "description": "Allows readonly monitoring and health checks for AI gateway resources", + "platforms": { + "aws": [ + { + "label": "check-bedrock-availability", + "description": "Read Bedrock foundation model availability needed for application health checks.", + "grant": { + "actions": ["bedrock:ListFoundationModels"] + }, + "binding": { + "stack": { + "resources": ["arn:aws:bedrock:*::foundation-model/*"] + }, + "resource": { + "resources": ["arn:aws:bedrock:*::foundation-model/*"] + } + } + } + ], + "gcp": [ + { + "label": "check-vertex-ai-health", + "description": "Read Vertex AI model and endpoint metadata needed for application health checks.", + "grant": { + "permissions": ["aiplatform.models.list", "aiplatform.endpoints.list"] + }, + "binding": { + "stack": { "scope": "projects/${projectName}" }, + "resource": { "scope": "projects/${projectName}" } + } + } + ], + "azure": [ + { + "label": "check-cognitive-services-health", + "description": "Read Azure Cognitive Services account metadata needed for application health checks.", + "grant": { + "actions": ["Microsoft.CognitiveServices/accounts/read"] + }, + "binding": { + "stack": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}" + }, + "resource": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.CognitiveServices/accounts/${resourceName}" + } + } + } + ] + } +} diff --git a/crates/alien-permissions/permission-sets/ai/invoke.jsonc b/crates/alien-permissions/permission-sets/ai/invoke.jsonc new file mode 100644 index 000000000..3360bcab5 --- /dev/null +++ b/crates/alien-permissions/permission-sets/ai/invoke.jsonc @@ -0,0 +1,79 @@ +{ + "id": "ai/invoke", + "description": "Allows invoking AI models for inference (no deployment writes)", + "platforms": { + "aws": [ + { + "grant": { + "actions": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock-mantle:CreateInference" + ] + }, + // AWS Bedrock foundation models are not per-account or per-stack resources; + // they are a global namespace. Claude models are invoked via cross-region + // inference profiles (us.anthropic.* and the region-agnostic global.anthropic.*), + // which live in the invoking account and must be allowed alongside the + // foundation-model ARN in the same statement (AWS docs: "Prerequisites for + // inference profiles"). + // The bedrock-mantle endpoint (native Anthropic Messages + OpenAI Responses) + // authorizes CreateInference against a project resource. The gateway sends no + // project id, so requests use the account's system default project; scope the + // grant to that one project (project/default) rather than project/*, which + // would also cover any user-created bedrock-mantle project. + "binding": { + "stack": { + "resources": [ + "arn:aws:bedrock:*::foundation-model/*", + "arn:aws:bedrock:*:${awsAccountId}:inference-profile/*", + "arn:aws:bedrock-mantle:*:${awsAccountId}:project/default" + ] + }, + "resource": { + "resources": [ + "arn:aws:bedrock:*::foundation-model/*", + "arn:aws:bedrock:*:${awsAccountId}:inference-profile/*", + "arn:aws:bedrock-mantle:*:${awsAccountId}:project/default" + ] + } + } + } + ], + "gcp": [ + { + "grant": { + // Custom role containing only prediction permissions. roles/aiplatform.user + // includes control-plane write actions (endpoints.deploy/undeploy, models.upload/delete) + // and sensitive-data reads (datasets.export, featurestores.readFeatures, etc.) that + // a workload must never hold. A custom role with the two predict permissions is the + // minimum required for Vertex AI inference. + "permissions": ["aiplatform.endpoints.predict", "aiplatform.endpoints.explain"] + }, + // Vertex AI is a project-level service; IAM bindings are scoped to the project. + "binding": { + "stack": { "scope": "projects/${projectName}" }, + "resource": { "scope": "projects/${projectName}" } + } + } + ], + "azure": [ + { + "grant": { + // "Cognitive Services OpenAI User" is the least-privilege data-plane role for the + // OpenAI-compatible inference endpoint: it grants inference only, not deployment + // management or account configuration. + "predefinedRoles": ["Cognitive Services OpenAI User"] + }, + "binding": { + "stack": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}" + }, + "resource": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.CognitiveServices/accounts/${resourceName}" + } + } + } + ] + } +} diff --git a/crates/alien-permissions/permission-sets/ai/management.jsonc b/crates/alien-permissions/permission-sets/ai/management.jsonc new file mode 100644 index 000000000..6318c2635 --- /dev/null +++ b/crates/alien-permissions/permission-sets/ai/management.jsonc @@ -0,0 +1,49 @@ +{ + "id": "ai/management", + "description": "Allows reading AI gateway account configuration and listing available models", + "platforms": { + "aws": [ + { + "grant": { + "actions": ["bedrock:ListFoundationModels"] + }, + "binding": { + "stack": { + "resources": ["arn:aws:bedrock:*::foundation-model/*"] + }, + "resource": { + "resources": ["arn:aws:bedrock:*::foundation-model/*"] + } + } + } + ], + "gcp": [ + { + "grant": { + "permissions": ["aiplatform.models.list", "aiplatform.endpoints.list"] + }, + "binding": { + "stack": { "scope": "projects/${projectName}" }, + "resource": { "scope": "projects/${projectName}" } + } + } + ], + "azure": [ + { + "grant": { + // Read-only management: inspect the account configuration. Deploying the + // predefined model set is a provision-time action (ai/provision), not management. + "actions": ["Microsoft.CognitiveServices/accounts/read"] + }, + "binding": { + "stack": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}" + }, + "resource": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.CognitiveServices/accounts/${resourceName}" + } + } + } + ] + } +} diff --git a/crates/alien-permissions/permission-sets/ai/provision.jsonc b/crates/alien-permissions/permission-sets/ai/provision.jsonc new file mode 100644 index 000000000..341759534 --- /dev/null +++ b/crates/alien-permissions/permission-sets/ai/provision.jsonc @@ -0,0 +1,69 @@ +{ + "id": "ai/provision", + "description": "Allows provisioning and destroying AI gateway resources", + "platforms": { + "aws": [ + { + "grant": { + // Bedrock access is grantless at the infrastructure level; provisioning + // consists of attaching/detaching the bedrock-invoke policy on the + // workload IAM role created for this resource. + "actions": [ + "iam:PutRolePolicy", + "iam:GetRolePolicy", + "iam:DeleteRolePolicy", + "iam:ListRolePolicies" + ] + }, + "binding": { + "stack": { + "resources": ["arn:aws:iam::${awsAccountId}:role/${stackPrefix}-*"] + }, + "resource": { + "resources": ["arn:aws:iam::${awsAccountId}:role/${stackPrefix}-${resourceName}"] + } + } + } + ], + "gcp": [ + { + "grant": { + "permissions": [ + "serviceusage.services.enable", + "resourcemanager.projects.getIamPolicy", + "resourcemanager.projects.setIamPolicy" + ] + }, + "binding": { + "stack": { "scope": "projects/${projectName}" }, + "resource": { "scope": "projects/${projectName}" } + } + } + ], + "azure": [ + { + "grant": { + "actions": [ + "Microsoft.CognitiveServices/accounts/write", + "Microsoft.CognitiveServices/accounts/delete", + "Microsoft.CognitiveServices/accounts/read", + // Deploy and inspect the predefined model set at provision time. + "Microsoft.CognitiveServices/accounts/deployments/write", + "Microsoft.CognitiveServices/accounts/deployments/read", + "Microsoft.Authorization/roleAssignments/write", + "Microsoft.Authorization/roleAssignments/delete", + "Microsoft.Authorization/roleAssignments/read" + ] + }, + "binding": { + "stack": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}" + }, + "resource": { + "scope": "/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.CognitiveServices/accounts/${resourceName}" + } + } + } + ] + } +} diff --git a/crates/alien-permissions/src/generators/azure_runtime.rs b/crates/alien-permissions/src/generators/azure_runtime.rs index 8a2129782..a15dab1f9 100644 --- a/crates/alien-permissions/src/generators/azure_runtime.rs +++ b/crates/alien-permissions/src/generators/azure_runtime.rs @@ -582,6 +582,7 @@ pub fn azure_predefined_role_id(role_name: &str) -> Option<&'static str> { "AcrPush" => Some("8311e382-0749-4cb8-b61a-304f252e45ec"), "Azure Service Bus Data Receiver" => Some("4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0"), "Azure Service Bus Data Sender" => Some("69a216fc-b8fb-44d8-bc22-1f3c2cd27a39"), + "Cognitive Services OpenAI User" => Some("5e0bd9bd-7b93-4f28-af87-19fc36ad61bd"), "Key Vault Contributor" => Some("f25e0fa2-a7c8-4377-a976-54943a77a395"), "Key Vault Secrets User" => Some("4633458b-17de-408a-b874-0445c86b69e6"), "Managed Identity Contributor" => Some("e40ec5ca-96e0-45a2-b4ff-59039f2c2b59"), diff --git a/crates/alien-permissions/tests/aws_abac_validation.rs b/crates/alien-permissions/tests/aws_abac_validation.rs index 0728f320e..ba83fd575 100644 --- a/crates/alien-permissions/tests/aws_abac_validation.rs +++ b/crates/alien-permissions/tests/aws_abac_validation.rs @@ -296,7 +296,32 @@ fn aws_resource_arns_are_stack_or_resource_scoped_unless_documented_external() { fn documented_external_resource_scope(resource: &str) -> bool { // Runtime compute pulls images from the manager-owned artifact registry. Target-account // isolation is enforced by the repository resource policy that grants this role access. - resource == "arn:aws:ecr:*:${managingAccountId}:repository/*" + if resource == "arn:aws:ecr:*:${managingAccountId}:repository/*" { + return true; + } + + // AWS Bedrock foundation models are a global AWS namespace owned by AWS, not the + // customer account. There is no per-stack or per-account ARN to scope to; the + // wildcard path is required and does not widen customer data access. + if resource == "arn:aws:bedrock:*::foundation-model/*" { + return true; + } + + // AWS Bedrock cross-region inference profiles are system-defined by AWS + // (us.anthropic.*, etc.) and not per-stack resources. They live in the + // invoking account, so the ARN pins the account and wildcards only region + // and profile name. See AWS docs: "Prerequisites for inference profiles" + // (bedrock:InvokeModel* on both foundation-model/* and inference-profile/*). + if resource == "arn:aws:bedrock:*:${awsAccountId}:inference-profile/*" { + return true; + } + + // The bedrock-mantle endpoint (native Anthropic Messages + OpenAI Responses) + // authorizes inference against the account's system-defined default project + // (`project/default`), which, like inference profiles, exists per account + // rather than per stack. The grant is pinned to that one project (not + // `project/*`) since the gateway only ever uses the default project. + resource == "arn:aws:bedrock-mantle:*:${awsAccountId}:project/default" } fn documented_ses_domain_identity_scope(resource: &str) -> bool { diff --git a/crates/alien-permissions/tests/permission_set_validation.rs b/crates/alien-permissions/tests/permission_set_validation.rs index bcee96022..dad8b2453 100644 --- a/crates/alien-permissions/tests/permission_set_validation.rs +++ b/crates/alien-permissions/tests/permission_set_validation.rs @@ -341,10 +341,15 @@ fn validate_aws_actions( } } None => { - invalid_actions.push(format!( - "{} - service '{}' not found", - action, action_service - )); + // The pinned dataset can be missing a whole service, not just one + // of its actions, when the service is newer than the snapshot. A + // documented gap is still a real action. + if !is_known_aws_dataset_gap(action) { + invalid_actions.push(format!( + "{} - service '{}' not found", + action, action_service + )); + } } } } else { @@ -371,10 +376,18 @@ fn validate_aws_actions( } fn is_known_aws_dataset_gap(action: &str) -> bool { - // API Gateway v2 authorizes tagged CreateStage calls through TagResource. - // The pinned IAM dataset still models API Gateway mostly through method-like - // actions such as POST/PATCH and does not list this action. - matches!(action, "apigateway:TagResource") + matches!( + action, + // API Gateway v2 authorizes tagged CreateStage calls through TagResource. + // The pinned IAM dataset still models API Gateway mostly through method-like + // actions such as POST/PATCH and does not list this action. + | "apigateway:TagResource" + // bedrock-mantle (the OpenAI/Anthropic-compatible inference endpoint) is newer + // than the pinned dataset snapshot, which carries no entry for the service at + // all. The action is real: AWS's own AmazonBedrockMantleInferenceAccess managed + // policy grants bedrock-mantle:CreateInference. + | "bedrock-mantle:CreateInference" + ) } /// Validate GCP permissions in a permission set diff --git a/crates/alien-permissions/tests/registry_tests.rs b/crates/alien-permissions/tests/registry_tests.rs index 615f3d7b1..4874a588b 100644 --- a/crates/alien-permissions/tests/registry_tests.rs +++ b/crates/alien-permissions/tests/registry_tests.rs @@ -1,5 +1,256 @@ use alien_permissions::{get_permission_set, has_permission_set, list_permission_set_ids}; +#[test] +fn test_ai_permission_sets_resolve_via_registry() { + assert!(has_permission_set("ai/provision")); + assert!(has_permission_set("ai/management")); + assert!(has_permission_set("ai/heartbeat")); + assert!(has_permission_set("ai/invoke")); + + let ids = list_permission_set_ids(); + assert!(ids.contains(&"ai/provision")); + assert!(ids.contains(&"ai/management")); + assert!(ids.contains(&"ai/heartbeat")); + assert!(ids.contains(&"ai/invoke")); +} + +#[test] +fn test_ai_invoke_is_inference_only() { + let invoke = get_permission_set("ai/invoke").expect("ai/invoke must resolve"); + + // AWS: inference actions only, enforced as an explicit allowlist. A substring + // denylist ("contains Create/Delete/Deploy") both over- and under-fires: it + // rejects `bedrock-mantle:CreateInference`, which IS an inference call (it + // creates an inference — it runs the model), while still passing any + // control-plane action whose name happens to lack those words. Adding an entry + // here is deliberate: it must be a data-plane inference call, never a + // deployment or other control-plane write, which belong in ai/provision. + const AWS_INFERENCE_ACTIONS: &[&str] = &[ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock-mantle:CreateInference", + ]; + if let Some(aws) = &invoke.platforms.aws { + for entry in aws { + if let Some(actions) = &entry.grant.actions { + for action in actions { + assert!( + AWS_INFERENCE_ACTIONS.contains(&action.as_str()), + "ai/invoke AWS grant must contain only inference actions; found \ + {action}, which is not in {AWS_INFERENCE_ACTIONS:?}. If it is a \ + data-plane inference call, add it deliberately; deployment and \ + control-plane actions belong in ai/provision." + ); + } + } + } + } + + // GCP: must use a custom role (permissions list), never a predefined role. + // roles/aiplatform.user includes control-plane writes and sensitive-data reads + // that a workload must never hold. + if let Some(gcp) = &invoke.platforms.gcp { + for (i, entry) in gcp.iter().enumerate() { + assert!( + entry.grant.predefined_roles.is_none(), + "ai/invoke GCP entry {i} must not use predefinedRoles (grants over-broad control-plane access); use a permissions list instead" + ); + let permissions = entry.grant.permissions.as_ref().unwrap_or_else(|| { + panic!("ai/invoke GCP entry {i} must have a permissions list") + }); + for perm in permissions { + // No control-plane write actions. + assert!( + !perm.contains("deploy") && !perm.contains("upload") && !perm.contains("delete"), + "ai/invoke GCP grant must not contain control-plane write permissions, found: {perm}" + ); + // No sensitive-data read actions. + assert!( + !perm.starts_with("datasets.") + && !perm.contains("featurestores") + && !perm.contains("ragCorpora") + && !perm.contains("sessions"), + "ai/invoke GCP grant must not contain sensitive-data read permissions, found: {perm}" + ); + } + } + } + + // Azure: must NOT grant deployments/write (the key deploy-on-demand invariant) + if let Some(azure) = &invoke.platforms.azure { + for entry in azure { + if let Some(actions) = &entry.grant.actions { + for action in actions { + assert_ne!( + action, + "Microsoft.CognitiveServices/accounts/deployments/write", + "ai/invoke must not grant deployments/write; that belongs in ai/provision" + ); + } + } + // predefinedRoles on invoke must not be management-class roles + if let Some(roles) = &entry.grant.predefined_roles { + for role in roles { + assert_ne!( + role, "Contributor", + "ai/invoke must not use Contributor role" + ); + assert_ne!(role, "Owner", "ai/invoke must not use Owner role"); + } + } + } + } +} + +#[test] +fn test_ai_finetune_grants_job_lifecycle() { + let finetune = get_permission_set("ai/finetune").expect("ai/finetune must resolve"); + + // AWS: the Bedrock customization-job lifecycle plus custom-model invoke. + let aws = finetune.platforms.aws.as_ref().expect("aws platform"); + let aws_actions: Vec<&String> = aws + .iter() + .filter_map(|e| e.grant.actions.as_ref()) + .flatten() + .collect(); + for required in [ + "bedrock:CreateModelCustomizationJob", + "bedrock:GetModelCustomizationJob", + "bedrock:GetCustomModel", + ] { + assert!( + aws_actions.iter().any(|a| a.as_str() == required), + "ai/finetune AWS must grant {required}" + ); + } + + // GCP: the Vertex tuning-job lifecycle as a least-privilege permissions list, + // never a predefined role (same invariant ai/invoke holds). + let gcp = finetune.platforms.gcp.as_ref().expect("gcp platform"); + for (i, entry) in gcp.iter().enumerate() { + assert!( + entry.grant.predefined_roles.is_none(), + "ai/finetune GCP entry {i} must use a permissions list, not predefinedRoles" + ); + let perms = entry.grant.permissions.as_ref().expect("permissions list"); + assert!( + perms.iter().any(|p| p == "aiplatform.tuningJobs.create"), + "ai/finetune GCP must grant aiplatform.tuningJobs.create" + ); + } + + // Azure: fine-tuning + deployment need a management-class role, which is + // exactly what ai/invoke forbids — assert finetune is allowed to carry it. + let azure = finetune.platforms.azure.as_ref().expect("azure platform"); + let has_contributor = azure.iter().any(|e| { + e.grant + .predefined_roles + .as_ref() + .map(|roles| roles.iter().any(|r| r == "Cognitive Services Contributor")) + .unwrap_or(false) + }); + assert!( + has_contributor, + "ai/finetune Azure must grant Cognitive Services Contributor to create tuning jobs and deployments" + ); +} + +#[test] +fn test_ai_provision_has_deployment_writes() { + // The predefined model set is deployed at provision time, so deployments/{write,read} + // live in ai/provision (control plane), not in ai/management or ai/invoke. + let provision = get_permission_set("ai/provision").expect("ai/provision must resolve"); + let azure = provision + .platforms + .azure + .as_ref() + .expect("ai/provision must have Azure platform"); + let has_write = azure.iter().any(|entry| { + entry + .grant + .actions + .as_ref() + .map(|actions| { + actions.contains( + &"Microsoft.CognitiveServices/accounts/deployments/write".to_string(), + ) + }) + .unwrap_or(false) + }); + assert!( + has_write, + "ai/provision Azure must grant deployments/write for the predefined model set" + ); + + // ai/management is read-only metadata; deployment writes live in ai/provision. + let management = get_permission_set("ai/management").expect("ai/management must resolve"); + let mgmt_azure = management + .platforms + .azure + .as_ref() + .expect("ai/management must have Azure platform"); + let mgmt_has_write = mgmt_azure.iter().any(|entry| { + entry + .grant + .actions + .as_ref() + .map(|actions| { + actions.contains( + &"Microsoft.CognitiveServices/accounts/deployments/write".to_string(), + ) + }) + .unwrap_or(false) + }); + assert!( + !mgmt_has_write, + "ai/management must not grant deployments/write" + ); +} + +#[test] +fn test_ai_invoke_uses_openai_user_role() { + let invoke = get_permission_set("ai/invoke").expect("ai/invoke must resolve"); + let azure = invoke + .platforms + .azure + .as_ref() + .expect("ai/invoke must have Azure platform"); + let uses_openai_user = azure.iter().any(|entry| { + entry + .grant + .predefined_roles + .as_ref() + .map(|roles| roles.contains(&"Cognitive Services OpenAI User".to_string())) + .unwrap_or(false) + }); + assert!( + uses_openai_user, + "ai/invoke Azure must use the least-privilege 'Cognitive Services OpenAI User' role" + ); +} + +#[test] +fn test_openai_user_role_id_resolves() { + use alien_permissions::generators::azure_runtime::azure_predefined_role_id; + assert_eq!( + azure_predefined_role_id("Cognitive Services OpenAI User"), + Some("5e0bd9bd-7b93-4f28-af87-19fc36ad61bd"), + "the OpenAI-User role GUID must be registered" + ); +} + +#[test] +fn test_ai_permission_sets_have_all_platforms() { + for id in ["ai/provision", "ai/management", "ai/heartbeat", "ai/invoke", "ai/finetune"] { + let perm_set = get_permission_set(id).unwrap_or_else(|| panic!("{id} must resolve")); + assert_eq!(perm_set.id, id); + assert!(!perm_set.description.is_empty(), "{id} must have a description"); + assert!(perm_set.platforms.aws.is_some(), "{id} must have AWS platform"); + assert!(perm_set.platforms.gcp.is_some(), "{id} must have GCP platform"); + assert!(perm_set.platforms.azure.is_some(), "{id} must have Azure platform"); + } +} + #[test] fn test_registry_basic_functionality() { // Test that the registry contains expected permission sets diff --git a/crates/alien-preflights/src/compile_time/allowed_user_resources.rs b/crates/alien-preflights/src/compile_time/allowed_user_resources.rs index bd7af09fd..9d96c836f 100644 --- a/crates/alien-preflights/src/compile_time/allowed_user_resources.rs +++ b/crates/alien-preflights/src/compile_time/allowed_user_resources.rs @@ -39,6 +39,7 @@ impl CompileTimeCheck for AllowedUserResourcesCheck { // by the ComputeClusterMutation when not declared. "compute-cluster", "postgres", + "ai", "email", "experimental/aws-opensearch", ]); diff --git a/crates/alien-terraform/src/built_ins.rs b/crates/alien-terraform/src/built_ins.rs index df9650be1..7655670b7 100644 --- a/crates/alien-terraform/src/built_ins.rs +++ b/crates/alien-terraform/src/built_ins.rs @@ -5,9 +5,9 @@ use crate::registry::TfRegistry; use alien_core::{ - ArtifactRegistry, AzureContainerAppsEnvironment, AzureResourceGroup, AzureServiceBusNamespace, - AzureStorageAccount, Build, KubernetesCluster, Kv, Network, Platform, Queue, - RemoteStackManagement, ServiceAccount, ServiceActivation, Storage, Vault, Worker, + Ai, ArtifactRegistry, AzureContainerAppsEnvironment, AzureResourceGroup, + AzureServiceBusNamespace, AzureStorageAccount, Build, KubernetesCluster, Kv, Network, Platform, + Queue, RemoteStackManagement, ServiceAccount, ServiceActivation, Storage, Vault, Worker, }; pub(crate) fn register_all(registry: &mut TfRegistry) { @@ -19,6 +19,7 @@ pub(crate) fn register_all(registry: &mut TfRegistry) { fn register_aws(registry: &mut TfRegistry) { use crate::emitters::aws; let p = Platform::Aws; + registry.register(Ai::RESOURCE_TYPE, p, aws::AwsAiEmitter); registry.register(Storage::RESOURCE_TYPE, p, aws::AwsStorageEmitter); registry.register(Kv::RESOURCE_TYPE, p, aws::AwsKvEmitter); registry.register(Queue::RESOURCE_TYPE, p, aws::AwsQueueEmitter); @@ -51,6 +52,7 @@ fn register_aws(registry: &mut TfRegistry) { fn register_gcp(registry: &mut TfRegistry) { use crate::emitters::gcp; let p = Platform::Gcp; + registry.register(Ai::RESOURCE_TYPE, p, gcp::GcpAiEmitter); registry.register(Storage::RESOURCE_TYPE, p, gcp::GcpStorageEmitter); registry.register(Kv::RESOURCE_TYPE, p, gcp::GcpKvEmitter); registry.register(Queue::RESOURCE_TYPE, p, gcp::GcpQueueEmitter); @@ -90,6 +92,7 @@ fn register_azure(registry: &mut TfRegistry) { let p = Platform::Azure; // Main resources — one emitter per Alien resource type. + registry.register(Ai::RESOURCE_TYPE, p, azure::AzureAiEmitter); registry.register(Storage::RESOURCE_TYPE, p, azure::AzureStorageEmitter); registry.register(Kv::RESOURCE_TYPE, p, azure::AzureKvEmitter); registry.register(Queue::RESOURCE_TYPE, p, azure::AzureQueueEmitter); diff --git a/crates/alien-terraform/src/emitters/aws/ai.rs b/crates/alien-terraform/src/emitters/aws/ai.rs new file mode 100644 index 000000000..d1c80d991 --- /dev/null +++ b/crates/alien-terraform/src/emitters/aws/ai.rs @@ -0,0 +1,241 @@ +//! AWS AI — Bedrock inference gateway. +//! +//! AWS Bedrock is a regional, account-scoped service with no per-stack +//! cloud resource to provision. The emitter returns an empty fragment for +//! the resource itself and emits any resource-scoped `aws_iam_role_policy` +//! blocks for permission profiles that reference an `ai/*` set (e.g. +//! `ai/invoke`, or `ai/finetune` when the resource declares a fine-tuning +//! job) on this resource. The region is carried in the import ref so the +//! controller can reconstruct the Bedrock endpoint without a cloud round-trip. +//! +//! Stack-level permissions flow through `AwsServiceAccountEmitter` via +//! `stack_permission_sets`; resource-scoped grants are emitted here. + +use crate::{ + block::{attr, resource_block}, + emitter::{TfEmitter, TfFragment}, + emitters::aws::helpers::{ + aws_terraform_permission_context, downcast, emit_iam_role_policy_for_target_with_label, + iam_policy_name_sanitize, iam_role_name_template, jsonencode, required_label, + service_assume_role_policy, tags, + }, + expr, +}; +use alien_core::{ + import::EmitContext, Ai, PermissionProfile, PermissionSetReference, Result, ServiceAccount, + Storage, +}; +use alien_permissions::BindingTarget; +use hcl::expr::Expression; + +/// The `ai/finetune` permission set id. When a permission profile references it on +/// this AI resource, the emitter provisions a dedicated Bedrock-trusted IAM role so +/// Bedrock can assume it to read training data and write output. +const AI_FINETUNE_PERMISSION_ID: &str = "ai/finetune"; + +#[derive(Debug, Clone, Copy, Default)] +pub struct AwsAiEmitter; + +impl TfEmitter for AwsAiEmitter { + fn emit(&self, ctx: &EmitContext<'_>) -> Result { + let ai = downcast::(ctx, Ai::RESOURCE_TYPE)?; + let mut fragment = TfFragment::empty(); + + let context = aws_terraform_permission_context() + .with_resource_name(format!("${{local.resource_prefix}}-{}", ai.id())); + + for (owner_label, permission_set_refs) in ai_permission_owners(ctx) { + for (idx, permission_set_ref) in permission_set_refs.iter().enumerate() { + if let Some(permission_set) = permission_set_ref + .resolve(|name| alien_permissions::get_permission_set(name).cloned()) + { + let ai_label_segment = sanitize_label_segment(ai.id()); + emit_iam_role_policy_for_target_with_label( + &mut fragment, + &owner_label, + &permission_set, + &format!("{owner_label}_ai_{ai_label_segment}_set_{idx}"), + &format!( + "access-{}-{}", + ai.id(), + iam_policy_name_sanitize(&permission_set.id) + ), + &context, + BindingTarget::Resource, + )?; + } + } + } + + // When a permission profile references `ai/finetune` on this resource, emit a + // dedicated IAM role Bedrock can assume for model-customization jobs. The + // stack's service-account roles only trust compute principals + // (`lambda`/`codebuild`/`ec2`), so Bedrock cannot assume them — that is the + // real AccessDenied this role fixes. Its name matches the controller's + // `role_arn` (`{prefix}-{id}-finetune`) so the runtime gateway can pass it. + if resource_references_finetune(ctx) { + emit_finetune_role(&mut fragment, ctx, ai.id()); + } + + Ok(fragment) + } + + fn emit_import_ref(&self, ctx: &EmitContext<'_>) -> Result { + let _ = downcast::(ctx, Ai::RESOURCE_TYPE)?; + let _ = required_label(ctx)?; + Ok(expr::object([("region", expr::raw("data.aws_region.current.region"))])) + } +} + +fn ai_permission_owners(ctx: &EmitContext<'_>) -> Vec<(String, Vec)> { + let mut owners = Vec::new(); + for (profile_name, profile) in ctx.stack.permission_profiles() { + let refs = ai_permission_refs(profile, ctx.resource_id); + if refs.is_empty() { + continue; + } + let service_account_id = format!("{profile_name}-sa"); + if let Some(label) = service_account_label(ctx, &service_account_id) { + owners.push((label.to_string(), refs)); + } + } + owners +} + +fn ai_permission_refs(profile: &PermissionProfile, resource_id: &str) -> Vec { + let mut refs = Vec::new(); + let mut seen_ids = std::collections::HashSet::new(); + if let Some(resource_refs) = profile.0.get(resource_id) { + for permission_ref in resource_refs { + if seen_ids.insert(permission_ref.id().to_string()) { + refs.push(permission_ref.clone()); + } + } + } + refs +} + +fn service_account_label<'a>(ctx: &'a EmitContext<'_>, service_account_id: &str) -> Option<&'a str> { + let (_id, entry) = ctx + .stack + .resources() + .find(|(id, _entry)| id.as_str() == service_account_id)?; + entry.config.downcast_ref::()?; + ctx.name_for(service_account_id) +} + +fn sanitize_label_segment(input: &str) -> String { + input + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() { + ch.to_ascii_lowercase() + } else { + '_' + } + }) + .collect() +} + +/// True when any permission profile references the `ai/finetune` set on this AI +/// resource. Only then is the Bedrock-trusted finetune role needed. +fn resource_references_finetune(ctx: &EmitContext<'_>) -> bool { + ctx.stack.permission_profiles().values().any(|profile| { + ai_permission_refs(profile, ctx.resource_id) + .iter() + .any(|reference| reference.id() == AI_FINETUNE_PERMISSION_ID) + }) +} + +/// Emit the dedicated Bedrock-trusted finetune IAM role plus its inline S3 policy. +/// +/// The role is named `{prefix}-{id}-finetune` (matching the controller's +/// `role_arn`), trusts `bedrock.amazonaws.com`, and can read training data +/// (`s3:GetObject`/`s3:ListBucket`) and write output (`s3:PutObject`) on every +/// storage bucket in the stack. +fn emit_finetune_role(fragment: &mut TfFragment, ctx: &EmitContext<'_>, ai_id: &str) { + let role_label = format!("{}_finetune", sanitize_label_segment(ai_id)); + + fragment.resource_blocks.push(resource_block( + "aws_iam_role", + &role_label, + [ + attr( + "name", + iam_role_name_template(&format!("{ai_id}-finetune")), + ), + attr( + "assume_role_policy", + service_assume_role_policy(&["bedrock.amazonaws.com"]), + ), + attr("tags", tags(ctx, "ai")), + ], + )); + + let statements = finetune_s3_statements(ctx); + fragment.resource_blocks.push(resource_block( + "aws_iam_role_policy", + &format!("{role_label}_s3"), + [ + attr( + "name", + Expression::String(format!("{ai_id}-finetune-s3")), + ), + attr( + "role", + expr::traversal(["aws_iam_role", role_label.as_str(), "id"]), + ), + attr( + "policy", + jsonencode(expr::object([ + ("Version", Expression::String("2012-10-17".to_string())), + ("Statement", Expression::Array(statements)), + ])), + ), + ], + )); +} + +/// S3 statements scoping the finetune role to the stack's storage buckets: +/// list/read on the bucket + objects, put on objects (training in, output out). +fn finetune_s3_statements(ctx: &EmitContext<'_>) -> Vec { + let mut bucket_arns = Vec::new(); + let mut object_arns = Vec::new(); + for (id, entry) in ctx.stack.resources() { + if entry.config.downcast_ref::().is_none() { + continue; + } + let Some(label) = ctx.name_for(id) else { + continue; + }; + bucket_arns.push(expr::traversal(["aws_s3_bucket", label, "arn"])); + object_arns.push(expr::template(format!("${{aws_s3_bucket.{label}.arn}}/*"))); + } + + vec![ + // Read the training dataset: list the bucket and get objects. + expr::object([ + ("Effect", Expression::String("Allow".to_string())), + ( + "Action", + Expression::Array(vec![ + Expression::String("s3:GetObject".to_string()), + Expression::String("s3:ListBucket".to_string()), + ]), + ), + ( + "Resource", + Expression::Array(bucket_arns.iter().cloned().chain(object_arns.iter().cloned()).collect()), + ), + ]), + // Write the tuning-job output back to storage. + expr::object([ + ("Effect", Expression::String("Allow".to_string())), + ( + "Action", + Expression::String("s3:PutObject".to_string()), + ), + ("Resource", Expression::Array(object_arns)), + ]), + ] +} diff --git a/crates/alien-terraform/src/emitters/aws/mod.rs b/crates/alien-terraform/src/emitters/aws/mod.rs index a76076e60..a03691de1 100644 --- a/crates/alien-terraform/src/emitters/aws/mod.rs +++ b/crates/alien-terraform/src/emitters/aws/mod.rs @@ -4,6 +4,7 @@ //! returns `hcl::Block` / `hcl::Expression` directly (no intermediate IR). //! Shared helpers live in [`helpers`]. +pub mod ai; pub mod artifact_registry; pub mod build; pub mod helpers; @@ -16,6 +17,7 @@ pub mod storage; pub mod vault; pub mod worker; +pub use ai::AwsAiEmitter; pub use artifact_registry::AwsArtifactRegistryEmitter; pub use build::AwsBuildEmitter; pub use kv::AwsKvEmitter; diff --git a/crates/alien-terraform/src/emitters/azure/ai.rs b/crates/alien-terraform/src/emitters/azure/ai.rs new file mode 100644 index 000000000..761cb1edf --- /dev/null +++ b/crates/alien-terraform/src/emitters/azure/ai.rs @@ -0,0 +1,233 @@ +//! Azure AI — Azure AIServices (Cognitive Services) account. +//! +//! Mirrors `AzureAiController`: one `azurerm_cognitive_account` per `Ai` +//! resource, configured as `kind = "AIServices"` with SKU `S0` and a +//! `custom_subdomain_name` derived from the stack resource prefix and the +//! resource id. +//! +//! Resource-scoped role assignments (e.g. `Cognitive Services OpenAI User` for +//! `ai/invoke`) are emitted directly by this emitter, scoped to the cognitive +//! account, for each permission profile that references `ai/invoke` on this +//! resource. Stack-level permissions flow through `AzureServiceAccountEmitter` +//! via `stack_permission_sets`. + +use crate::{ + block::{attr, block, nested, resource_block}, + emitter::{TfEmitter, TfFragment}, + emitters::azure::helpers::{ + downcast, permission_context, required_label, resource_prefix_template, + service_account_principal_id, tags, + }, + expr, +}; +use alien_core::{import::EmitContext, Ai, ErrorData, PermissionSetReference, Result}; +use alien_error::{AlienError, Context}; +use alien_permissions::{ + generators::{AzureRoleDefinitionRef, AzureRuntimePermissionsGenerator}, + BindingTarget, +}; +use hcl::expr::Expression; + +#[derive(Debug, Clone, Copy, Default)] +pub struct AzureAiEmitter; + +impl TfEmitter for AzureAiEmitter { + fn emit(&self, ctx: &EmitContext<'_>) -> Result { + let ai = downcast::(ctx, Ai::RESOURCE_TYPE)?; + let label = required_label(ctx)?; + let mut fragment = TfFragment::default(); + + fragment.resource_blocks.push(resource_block( + "azurerm_cognitive_account", + label, + [ + attr("name", resource_prefix_template(ai.id())), + attr( + "resource_group_name", + expr::raw("var.azure_resource_group_name"), + ), + attr("location", expr::raw("var.azure_location")), + attr("kind", Expression::String("AIServices".to_string())), + attr("sku_name", Expression::String("S0".to_string())), + attr("custom_subdomain_name", resource_prefix_template(ai.id())), + attr("tags", tags(ctx, "ai")), + ], + )); + + // One deployment per curated model, mirroring AzureAiController's + // DeployingModels state. capacity = 1 matches the controller's + // DEFAULT_DEPLOYMENT_CAPACITY so the Frozen (Terraform) and Live + // (controller) paths provision the same throughput. + for (deployment, model, version) in alien_core::ai_catalog::azure_deployments() { + fragment.resource_blocks.push(resource_block( + "azurerm_cognitive_deployment", + // The Azure deployment name keeps dots (e.g. gpt-4.1); the Terraform + // resource label cannot, so sanitize it. + &format!("{label}_{}", sanitize_deployment_label(deployment)), + [ + attr("name", Expression::String(deployment.to_string())), + attr( + "cognitive_account_id", + expr::traversal(["azurerm_cognitive_account", label, "id"]), + ), + nested(block( + "model", + [ + attr("format", Expression::String("OpenAI".to_string())), + attr("name", Expression::String(model.to_string())), + attr("version", Expression::String(version.to_string())), + ], + )), + nested(block( + "sku", + [ + attr("name", Expression::String("GlobalStandard".to_string())), + attr("capacity", expr::raw("1")), + ], + )), + ], + )); + } + + emit_ai_permissions(ctx, label, &mut fragment)?; + + Ok(fragment) + } + + fn emit_import_ref(&self, ctx: &EmitContext<'_>) -> Result { + let _ = downcast::(ctx, Ai::RESOURCE_TYPE)?; + let label = required_label(ctx)?; + Ok(expr::object([ + ("subscriptionId", expr::raw("var.azure_subscription_id")), + ("resourceGroup", expr::raw("var.azure_resource_group_name")), + ( + "accountName", + expr::traversal(["azurerm_cognitive_account", label, "name"]), + ), + ( + "endpoint", + expr::traversal(["azurerm_cognitive_account", label, "endpoint"]), + ), + ("location", expr::raw("var.azure_location")), + ])) + } +} + +/// Emit `azurerm_role_assignment` blocks for permission profiles that reference +/// `ai/invoke` (or any `ai/`-prefixed set) scoped to this resource. +fn emit_ai_permissions( + ctx: &EmitContext<'_>, + ai_label: &str, + fragment: &mut TfFragment, +) -> Result<()> { + for (profile_name, profile) in ctx.stack.permission_profiles() { + let Some(principal_id_expr) = service_account_principal_id(ctx, profile_name) else { + continue; + }; + + let refs: Vec<&PermissionSetReference> = profile + .0 + .get(ctx.resource_id) + .map(|refs| refs.iter().collect()) + .unwrap_or_default(); + + for permission_set_ref in refs { + let Some(permission_set) = permission_set_ref + .resolve(|name| alien_permissions::get_permission_set(name).cloned()) + else { + continue; + }; + if permission_set.platforms.azure.is_none() { + continue; + } + + let generator = AzureRuntimePermissionsGenerator::new(); + let perm_context = permission_context(ai_label).with_resource_name( + format!("${{local.resource_prefix}}-{}", ctx.resource_id), + ); + let grant_plan = generator + .generate_grant_plan(&permission_set, BindingTarget::Resource, &perm_context) + .context(ErrorData::GenericError { + message: format!( + "failed to generate Azure AI grants for '{}'", + permission_set.id + ), + })?; + + for (binding_index, binding) in grant_plan.bindings.iter().enumerate() { + let role_definition_id = match &binding.role_definition { + AzureRoleDefinitionRef::Predefined { role_definition_id } => { + expr::template(role_definition_id.clone()) + } + AzureRoleDefinitionRef::Custom { key } => { + // Custom role definitions for resource-scoped AI grants are + // emitted by emit_azure_setup_resource_role_definitions. + let index = grant_plan + .custom_roles + .iter() + .position(|role| role.key == *key) + .ok_or_else(|| AlienError::new(ErrorData::GenericError { + message: format!( + "custom role '{key}' not found in grant plan for Azure AI resource '{}'", + ctx.resource_id + ), + }))?; + let role_label = crate::emitters::azure::helpers::setup_execution_role_label( + profile_name, + &binding.role_name, + index, + ); + expr::traversal([ + "azurerm_role_definition", + role_label.as_str(), + "role_definition_resource_id", + ]) + } + }; + + let role_label = sanitize_role_label(&binding.role_name); + fragment.resource_blocks.push(resource_block( + "azurerm_role_assignment", + &format!("{ai_label}_{role_label}_{profile_name}_assignment_{binding_index}"), + [ + attr( + "name", + expr::raw(&format!( + "uuidv5(\"oid\", \"deployment:azure:ai-role-assign:${{azurerm_cognitive_account.{ai_label}.id}}:{role_label}:{profile_name}:{binding_index}\")" + )), + ), + attr( + "scope", + expr::traversal(["azurerm_cognitive_account", ai_label, "id"]), + ), + attr("role_definition_id", role_definition_id), + attr("principal_id", principal_id_expr.clone()), + ], + )); + } + } + } + + Ok(()) +} + +/// Turn a model deployment name into a valid Terraform resource label (letters, +/// digits, underscores, dashes): `gpt-4.1` becomes `gpt-4_1`. +fn sanitize_deployment_label(name: &str) -> String { + name.chars() + .map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' }) + .collect() +} + +fn sanitize_role_label(input: &str) -> String { + input + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() { + ch.to_ascii_lowercase() + } else { + '_' + } + }) + .collect() +} diff --git a/crates/alien-terraform/src/emitters/azure/mod.rs b/crates/alien-terraform/src/emitters/azure/mod.rs index 56d0cb0d8..ff18bd49d 100644 --- a/crates/alien-terraform/src/emitters/azure/mod.rs +++ b/crates/alien-terraform/src/emitters/azure/mod.rs @@ -11,6 +11,7 @@ //! Per-resource design notes cover storage-account naming convergence, //! cross-tenant federated-identity trust, and AKS overlay activation. +pub mod ai; pub mod artifact_registry; pub mod build; pub mod container_apps_environment; @@ -28,6 +29,7 @@ pub mod storage_account; pub mod vault; pub mod worker; +pub use ai::AzureAiEmitter; pub use artifact_registry::AzureArtifactRegistryEmitter; pub use build::AzureBuildEmitter; pub use container_apps_environment::AzureContainerAppsEnvironmentEmitter; diff --git a/crates/alien-terraform/src/emitters/gcp/ai.rs b/crates/alien-terraform/src/emitters/gcp/ai.rs new file mode 100644 index 000000000..82e7fcd09 --- /dev/null +++ b/crates/alien-terraform/src/emitters/gcp/ai.rs @@ -0,0 +1,39 @@ +//! GCP AI — Vertex AI inference gateway. +//! +//! Vertex AI is a project-level service with no per-stack resource to +//! provision. The emitter returns an empty fragment and carries the project +//! and location in the import ref so the controller can reconstruct the +//! Vertex AI endpoint without a cloud round-trip. +//! +//! The `aiplatform.googleapis.com` API enablement is handled by the +//! `GcpServiceActivationEmitter` when the preflight injects a +//! `ServiceActivation` for that API. The `ai/invoke` custom IAM role (predict +//! only) is emitted by `GcpServiceAccountEmitter` when a permission profile +//! references `ai/invoke`. + +use crate::{ + emitter::{TfEmitter, TfFragment}, + emitters::gcp::helpers::{downcast, required_label}, + expr, +}; +use alien_core::{import::EmitContext, Ai, Result}; +use hcl::expr::Expression; + +#[derive(Debug, Clone, Copy, Default)] +pub struct GcpAiEmitter; + +impl TfEmitter for GcpAiEmitter { + fn emit(&self, ctx: &EmitContext<'_>) -> Result { + let _ = downcast::(ctx, Ai::RESOURCE_TYPE)?; + Ok(TfFragment::empty()) + } + + fn emit_import_ref(&self, ctx: &EmitContext<'_>) -> Result { + let _ = downcast::(ctx, Ai::RESOURCE_TYPE)?; + let _ = required_label(ctx)?; + Ok(expr::object([ + ("projectId", expr::raw("var.gcp_project")), + ("location", expr::raw("var.gcp_region")), + ])) + } +} diff --git a/crates/alien-terraform/src/emitters/gcp/mod.rs b/crates/alien-terraform/src/emitters/gcp/mod.rs index e1306308c..a4c4b2146 100644 --- a/crates/alien-terraform/src/emitters/gcp/mod.rs +++ b/crates/alien-terraform/src/emitters/gcp/mod.rs @@ -5,6 +5,7 @@ //! (downcast, labels, IAM member binding, service-account email //! resolution) live in [`helpers`]. +pub mod ai; pub mod artifact_registry; pub mod build; pub mod helpers; @@ -18,6 +19,7 @@ pub mod storage; pub mod vault; pub mod worker; +pub use ai::GcpAiEmitter; pub use artifact_registry::GcpArtifactRegistryEmitter; pub use build::GcpBuildEmitter; pub use kv::GcpKvEmitter; diff --git a/crates/alien-terraform/tests/generator/aws_data_layer_tests.rs b/crates/alien-terraform/tests/generator/aws_data_layer_tests.rs index 05bdecd41..e135cd63d 100644 --- a/crates/alien-terraform/tests/generator/aws_data_layer_tests.rs +++ b/crates/alien-terraform/tests/generator/aws_data_layer_tests.rs @@ -1,4 +1,4 @@ -//! AWS data-layer scenarios — storage / kv / queue / vault. +//! AWS data-layer scenarios — storage / kv / queue / vault / ai. //! //! Each scenario is one multi-file snapshot so reviewers see the //! complete module a developer would `terraform apply`. Every scenario @@ -7,8 +7,8 @@ use super::helpers::{assert_terraform_valid, render, snapshot_module}; use alien_core::{ - Kv, LifecycleRule, PermissionProfile, Queue, ResourceLifecycle, ServiceAccount, Stack, - StackSettings, Storage, Vault, + Ai, FinetuneMethod, FinetuneSpec, Kv, LifecycleRule, PermissionProfile, Queue, + ResourceLifecycle, ServiceAccount, Stack, StackSettings, Storage, Vault, }; use alien_terraform::TerraformTarget; @@ -148,3 +148,128 @@ fn aws_data_layer_renders_complete_stack() { snapshot_module("aws_data_layer_full", &module); assert_terraform_valid(&module, "aws_data_layer_full"); } + +#[test] +fn aws_ai_emits_only_import_data() { + // AWS Bedrock has no per-stack cloud resource to provision. The emitter + // returns an empty fragment so only the import metadata JSON is produced. + let stack = Stack::new("acme-ai".to_string()) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + let module = render(&stack, TerraformTarget::Aws, StackSettings::default()); + snapshot_module("aws_ai_minimal", &module); + assert_terraform_valid(&module, "aws_ai_minimal"); + + // Import metadata must carry the region so the controller can reconstruct + // the Bedrock endpoint. The import ref appears in locals.tf. + let locals = module + .get("locals.tf") + .expect("locals.tf should render"); + assert!(locals.contains("region"), "import ref must carry region"); +} + +#[test] +fn aws_ai_invoke_permissions_attach_to_service_account_role() { + // When a permission profile references ai/invoke, the AI emitter attaches the + // bedrock IAM policy to the workload (service-account) role. + let stack = Stack::new("acme-ai".to_string()) + .permission( + "execution", + PermissionProfile::new().resource("llm", ["ai/invoke"]), + ) + .add( + ServiceAccount::new("execution-sa".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + let module = render(&stack, TerraformTarget::Aws, StackSettings::default()); + let rendered = module + .iter() + .map(|(_, contents)| contents) + .collect::(); + + assert!( + rendered.contains("bedrock:InvokeModel"), + "bedrock InvokeModel action must appear" + ); + assert!( + rendered.contains("bedrock:InvokeModelWithResponseStream"), + "bedrock InvokeModelWithResponseStream action must appear" + ); + assert!( + rendered.contains("arn:aws:bedrock:*::foundation-model/*"), + "bedrock foundation-model ARN must appear" + ); + assert_terraform_valid(&module, "aws_ai_invoke_permissions"); +} + +#[test] +fn aws_ai_finetune_emits_bedrock_trusted_role_with_s3_policy() { + // When a permission profile references ai/finetune on the AI resource, the + // emitter provisions a dedicated IAM role Bedrock can assume (the real fix for + // AccessDenied: service-account roles only trust compute principals) with an + // inline S3 policy over the stack's storage buckets. + let stack = Stack::new("acme-ai".to_string()) + .permission( + "execution", + PermissionProfile::new().resource("llm", ["ai/finetune"]), + ) + .add( + ServiceAccount::new("execution-sa".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add( + Storage::new("training".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add( + Ai::new("llm".to_string()) + .finetune(FinetuneSpec { + base_model: "amazon.nova-lite-v1:0".to_string(), + training_data: "training".to_string(), + training_key: "training.jsonl".to_string(), + served_model_id: None, + method: FinetuneMethod::Sft, + }) + .build(), + ResourceLifecycle::Frozen, + ) + .build(); + let module = render(&stack, TerraformTarget::Aws, StackSettings::default()); + let rendered = module + .iter() + .map(|(_, contents)| contents) + .collect::(); + + // The dedicated finetune role exists, named deterministically to match the + // controller's role_arn (`${resource_prefix}-llm-finetune`). + assert!( + rendered.contains("llm_finetune"), + "expected a dedicated finetune aws_iam_role:\n{rendered}" + ); + assert!( + rendered.contains("llm-finetune"), + "expected role name suffix llm-finetune matching the controller role_arn:\n{rendered}" + ); + // Its trust policy allows Bedrock to assume it — the crux of the fix. + assert!( + rendered.contains("bedrock.amazonaws.com"), + "finetune role must trust bedrock.amazonaws.com:\n{rendered}" + ); + // The inline policy grants S3 read (training data) and write (output), + // scoped to the stack's storage bucket ARN (not "*"). + assert!( + rendered.contains("s3:GetObject") && rendered.contains("s3:ListBucket"), + "finetune role must read the training dataset from S3:\n{rendered}" + ); + assert!( + rendered.contains("s3:PutObject"), + "finetune role must write tuning output to S3:\n{rendered}" + ); + assert!( + rendered.contains("aws_s3_bucket.training.arn"), + "S3 grants must reference the storage bucket ARN, not \"*\":\n{rendered}" + ); + assert_terraform_valid(&module, "aws_ai_finetune_role"); +} diff --git a/crates/alien-terraform/tests/generator/azure_data_layer_tests.rs b/crates/alien-terraform/tests/generator/azure_data_layer_tests.rs index 3741d809c..f881f04e4 100644 --- a/crates/alien-terraform/tests/generator/azure_data_layer_tests.rs +++ b/crates/alien-terraform/tests/generator/azure_data_layer_tests.rs @@ -1,4 +1,4 @@ -//! Azure data-layer scenarios — storage / kv / queue / vault. +//! Azure data-layer scenarios — storage / kv / queue / vault / ai. //! //! Mirror of `gcp_data_layer_tests.rs` for Azure. Each scenario is a //! single multi-file snapshot — the security team reads the full @@ -13,7 +13,7 @@ use super::helpers::{assert_terraform_valid, render, snapshot_module}; use alien_core::{ - AzureResourceGroup, AzureServiceBusNamespace, AzureStorageAccount, Kv, LifecycleRule, + Ai, AzureResourceGroup, AzureServiceBusNamespace, AzureStorageAccount, Kv, LifecycleRule, PermissionProfile, Queue, ResourceLifecycle, ResourceRef, ServiceAccount, Stack, StackSettings, Storage, Vault, }; @@ -274,3 +274,81 @@ fn azure_data_layer_renders_complete_stack() { snapshot_module("azure_data_layer_full", &module); assert_terraform_valid(&module, "azure_data_layer_full"); } + +#[test] +fn azure_ai_renders_cognitive_account() { + // Azure AI provisions an azurerm_cognitive_account (kind=AIServices, sku=S0). + let stack = Stack::new("acme-ai".to_string()) + .add(resource_group(), ResourceLifecycle::Frozen) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + let module = render(&stack, TerraformTarget::Azure, StackSettings::default()); + snapshot_module("azure_ai_minimal", &module); + assert_terraform_valid(&module, "azure_ai_minimal"); + + let rendered = module + .iter() + .map(|(_, contents)| contents) + .collect::(); + assert!( + rendered.contains("azurerm_cognitive_account"), + "must emit azurerm_cognitive_account" + ); + assert!(rendered.contains("AIServices"), "kind must be AIServices"); + assert!(rendered.contains("S0"), "sku_name must be S0"); + + assert!( + rendered.contains("azurerm_cognitive_deployment"), + "must emit a model deployment" + ); + assert!(rendered.contains("gpt-4.1"), "must deploy the curated gpt-4.1 model"); + assert!( + rendered.contains("cognitive_account_id"), + "deployment must reference the account via cognitive_account_id" + ); + assert!( + rendered.contains("GlobalStandard"), + "deployment sku must be GlobalStandard" + ); + + // Import metadata must carry accountName, endpoint, resourceGroup, location. + // The import ref appears in locals.tf. + let locals = module + .get("locals.tf") + .expect("locals.tf should render"); + assert!(locals.contains("accountName"), "import ref must carry accountName"); + assert!(locals.contains("endpoint"), "import ref must carry endpoint"); + assert!(locals.contains("resourceGroup"), "import ref must carry resourceGroup"); + assert!(locals.contains("location"), "import ref must carry location"); +} + +#[test] +fn azure_ai_invoke_permissions_emit_cognitive_services_openai_user_role() { + // When a permission profile references ai/invoke, the AI emitter emits a + // Cognitive Services OpenAI User role assignment scoped to the cognitive + // account, bound to the workload service account. + let stack = Stack::new("acme-ai".to_string()) + .permissions(alien_core::PermissionsConfig::new().with_profile( + "execution", + PermissionProfile::new().resource("llm", ["ai/invoke"]), + )) + .add(resource_group(), ResourceLifecycle::Frozen) + .add( + ServiceAccount::new("execution-sa".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + let module = render(&stack, TerraformTarget::Azure, StackSettings::default()); + let rendered = module + .iter() + .map(|(_, contents)| contents) + .collect::(); + + // The predefined role ID for "Cognitive Services OpenAI User" must appear. + assert!( + rendered.contains("5e0bd9bd-7b93-4f28-af87-19fc36ad61bd"), + "Cognitive Services OpenAI User role ID must appear" + ); + assert_terraform_valid(&module, "azure_ai_invoke_permissions"); +} diff --git a/crates/alien-terraform/tests/generator/gcp_data_layer_tests.rs b/crates/alien-terraform/tests/generator/gcp_data_layer_tests.rs index cc737294b..ab1201aaa 100644 --- a/crates/alien-terraform/tests/generator/gcp_data_layer_tests.rs +++ b/crates/alien-terraform/tests/generator/gcp_data_layer_tests.rs @@ -1,4 +1,4 @@ -//! GCP data-layer scenarios — storage / kv / queue / vault. +//! GCP data-layer scenarios — storage / kv / queue / vault / ai. //! //! Each scenario is one multi-file snapshot so reviewers see the //! complete module a developer would `terraform apply`. Every scenario @@ -7,7 +7,7 @@ use super::helpers::{assert_terraform_valid, render, snapshot_module}; use alien_core::{ - Kv, LifecycleRule, ManagementPermissions, PermissionProfile, PermissionsConfig, Queue, + Ai, Kv, LifecycleRule, ManagementPermissions, PermissionProfile, PermissionsConfig, Queue, RemoteStackManagement, ResourceLifecycle, ServiceAccount, Stack, StackSettings, Storage, Vault, }; use alien_terraform::TerraformTarget; @@ -204,3 +204,56 @@ fn gcp_data_layer_renders_complete_stack() { snapshot_module("gcp_data_layer_full", &module); assert_terraform_valid(&module, "gcp_data_layer_full"); } + +#[test] +fn gcp_ai_emits_only_import_data() { + // GCP Vertex AI has no per-stack cloud resource to provision. The emitter + // returns an empty fragment so only the import metadata JSON is produced. + let stack = Stack::new("acme-ai".to_string()) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + let module = render(&stack, TerraformTarget::Gcp, StackSettings::default()); + snapshot_module("gcp_ai_minimal", &module); + assert_terraform_valid(&module, "gcp_ai_minimal"); + + // Import metadata must carry project and location so the controller can + // reconstruct the Vertex AI endpoint. The import ref appears in locals.tf. + let locals = module + .get("locals.tf") + .expect("locals.tf should render"); + assert!(locals.contains("projectId"), "import ref must carry projectId"); + assert!(locals.contains("location"), "import ref must carry location"); +} + +#[test] +fn gcp_ai_invoke_permissions_attach_to_service_account() { + // When a permission profile references ai/invoke, the AI emitter emits a custom + // role containing only predict permissions (not the over-broad + // roles/aiplatform.user) and binds it to the workload service account. + let stack = Stack::new("acme-ai".to_string()) + .permissions(PermissionsConfig::new().with_profile( + "execution", + PermissionProfile::new().resource("llm", ["ai/invoke"]), + )) + .add( + ServiceAccount::new("execution-sa".to_string()).build(), + ResourceLifecycle::Frozen, + ) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + let module = render(&stack, TerraformTarget::Gcp, StackSettings::default()); + let rendered = module + .iter() + .map(|(_, contents)| contents) + .collect::(); + + assert!( + !rendered.contains("roles/aiplatform.user"), + "roles/aiplatform.user must NOT appear in rendered output; ai/invoke uses a least-privilege custom role" + ); + assert!( + rendered.contains("aiplatform.endpoints.predict"), + "aiplatform.endpoints.predict must appear in the custom role in rendered output" + ); + assert_terraform_valid(&module, "gcp_ai_invoke_permissions"); +} diff --git a/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__aws_ai_minimal.snap b/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__aws_ai_minimal.snap new file mode 100644 index 000000000..11882948c --- /dev/null +++ b/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__aws_ai_minimal.snap @@ -0,0 +1,290 @@ +--- +source: crates/alien-terraform/tests/generator/helpers.rs +expression: buf +--- +=== versions.tf === +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.6" + } + } +} + +=== variables.tf === +variable "resource_prefix" { + type = string + description = "Optional stable physical resource prefix. Leave empty to generate one." + default = "" + + validation { + condition = var.resource_prefix == "" || (can(regex("^[a-z][a-z0-9-]{1,38}[a-z0-9]$", var.resource_prefix)) && length(regexall("--", var.resource_prefix)) == 0) + error_message = "resource_prefix must be 3-40 characters: lowercase letters, numbers, and hyphens; start with a letter; end with a letter or number; and not contain consecutive hyphens." + } +} + +variable "name" { + type = string + description = "Human-readable application name shown in setup and cloud IAM review metadata." +} + +variable "token" { + type = string + description = "Install token from the application setup page. This is the same token used by the deploy CLI --token flag." + sensitive = true +} + +variable "management_url" { + type = string + description = "Optional management endpoint used by pull-style runtimes." + default = "" +} + +variable "deployment_model" { + type = string + description = "How runtime updates are delivered after setup." + default = "push" + + validation { + condition = contains(["push", "pull"], var.deployment_model) + error_message = "deployment_model must be one of: push, pull." + } +} + +variable "advanced_settings_json" { + type = string + description = "Advanced JSON-encoded deployment settings. Most installations should use the typed variables in this module instead." + default = "{}" + sensitive = true +} + +variable "advanced_settings_overlay_json" { + type = string + description = "JSON-encoded deployment settings merged over the package defaults. Use this for partial advanced-setting overrides that must preserve generated defaults such as compute selections." + default = "{}" + sensitive = true +} + +variable "updates_mode" { + type = string + description = "How application updates are delivered after setup." + default = "auto" + + validation { + condition = contains(["auto", "approval-required"], var.updates_mode) + error_message = "updates_mode must be one of: auto, approval-required." + } +} + +variable "telemetry_mode" { + type = string + description = "How logs, metrics, and traces are collected." + default = "auto" + + validation { + condition = contains(["off", "auto", "approval-required"], var.telemetry_mode) + error_message = "telemetry_mode must be one of: off, auto, approval-required." + } +} + +variable "heartbeats_mode" { + type = string + description = "Whether runtime health checks are enabled." + default = "on" + + validation { + condition = contains(["off", "on"], var.heartbeats_mode) + error_message = "heartbeats_mode must be one of: off, on." + } +} + +variable "aws_region" { + type = string + description = "AWS region used by the AWS provider." + default = "us-east-1" +} + +variable "managing_role_arn" { + type = string + description = "ARN of the management identity allowed to assume setup-created roles." + default = "" +} + +variable "managing_account_id" { + type = string + description = "AWS account ID that hosts application container images. Empty disables scoped cross-account image-pull grants." + default = "" +} + +=== providers.tf === +provider "aws" { + region = var.aws_region +} + +data "aws_caller_identity" "current" {} + +data "aws_region" "current" {} + +=== resource_prefix.tf === +resource "random_id" "resource_prefix" { + byte_length = 4 +} + +=== locals.tf === +locals { + resource_prefix = var.resource_prefix == "" ? format("a%s", random_id.resource_prefix.hex) : var.resource_prefix + deployment_name = var.name + deployment_platform = "aws" + deployment_target = "aws" + deployment_region = data.aws_region.current.region + deployment_management_config = null + advanced_settings = merge(jsondecode(var.advanced_settings_json), jsondecode(var.advanced_settings_overlay_json)) + deployment_settings = merge(local.advanced_settings, { deploymentModel = var.deployment_model, updates = var.updates_mode, telemetry = var.telemetry_mode, heartbeats = var.heartbeats_mode }) + deployment_resources = [ + { + id = "llm" + type = "ai" + importData = { + region = data.aws_region.current.region + } + } + ] +} + +=== registration.tf === +resource "terraform_data" "deployment_registration" { + input = { + platform = local.deployment_platform + token = var.token + name = var.name + resource_prefix = local.resource_prefix + setup_target = "" + setup_import_format_version = 1 + setup_fingerprint = "" + setup_fingerprint_version = 0 + management_url = var.management_url + management_config = local.deployment_management_config + stack_settings = local.deployment_settings + resources = local.deployment_resources + inputValues = {} + } +} + +=== outputs.tf === +output "deployment_target" { + value = "aws" + description = "Terraform module target." +} + +output "deployment_resource_prefix" { + value = local.resource_prefix + description = "Physical resource prefix." +} + +output "deployment_platform" { + value = local.deployment_platform + description = "Target platform." +} + +output "deployment_region" { + value = local.deployment_region + description = "Target cloud region or location." +} + +output "deployment_setup_target" { + value = "" + description = "Setup target." +} + +output "deployment_setup_import_format_version" { + value = 1 + description = "Setup registration payload format version." +} + +output "deployment_setup_fingerprint" { + value = "" + description = "Setup compatibility fingerprint." +} + +output "deployment_setup_fingerprint_version" { + value = 0 + description = "Setup fingerprint algorithm version." +} + +output "deployment_management_config" { + value = jsonencode(local.deployment_management_config) + description = "Deployment registration management configuration JSON." +} + +output "deployment_stack_settings" { + value = jsonencode(local.deployment_settings) + description = "Deployment registration settings JSON." + sensitive = true +} + +output "deployment_resources" { + value = jsonencode(local.deployment_resources) + description = "Deployment registration resource metadata JSON." +} + +=== README.md === +# Deployment setup - acme-ai + +Target: `aws`. + +This module creates setup-owned infrastructure, grants the management access needed after setup, and prepares deployment registration metadata. Review the generated `.tf` files before applying; each resource file maps to one setup resource. + +## Inputs + +Required: + +- `token`: install token from the setup page. +- `name`: deployment name to include in the registration metadata. + +Common optional settings: + +- `resource_prefix`: stable physical-name prefix. Leave empty to generate one. +- `management_url`: optional management endpoint used by pull-style runtimes. +- `deployment_model`: `push` or `pull`. +- `updates_mode`: `auto` or `approval-required`. +- `telemetry_mode`: `off`, `auto`, or `approval-required`. +- `heartbeats_mode`: `off` or `on`. +- `advanced_settings_json`: complete advanced deployment settings JSON. Most installs should keep the generated default. +- `advanced_settings_overlay_json`: partial advanced settings merged over package defaults, preserving generated values such as compute selections. + +AWS settings: + +- `aws_region`: AWS region used by the provider. +- `managing_role_arn`: management identity allowed to assume setup-created roles. +- `managing_account_id`: account that hosts application container images. Empty disables scoped cross-account image-pull grants. + +## Run + +Use your organization's normal backend and approval workflow. A typical local review looks like: + +```bash +export TF_VAR_name="acme-ai" +export TF_VAR_token="..." +terraform init +terraform validate +terraform plan -out=tfplan +terraform apply tfplan +``` + +## Registration + +This module exposes `deployment_management_config`, `deployment_stack_settings`, and `deployment_resources` outputs for registration flows managed outside Terraform. + +## Outputs + +- `deployment_management_config`: management endpoint and credential-boundary metadata. +- `deployment_stack_settings`: deployment settings JSON assembled from typed variables, package defaults, and advanced-setting overlays. +- `deployment_resources`: setup-owned resource metadata handed to the deployment runtime. +- `deployment_id` and `deployment_token`: emitted only when Terraform performs registration. diff --git a/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__azure_ai_minimal.snap b/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__azure_ai_minimal.snap new file mode 100644 index 000000000..c4bdef90b --- /dev/null +++ b/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__azure_ai_minimal.snap @@ -0,0 +1,398 @@ +--- +source: crates/alien-terraform/tests/generator/helpers.rs +expression: buf +--- +=== versions.tf === +terraform { + required_version = ">= 1.5.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.100" + } + random = { + source = "hashicorp/random" + version = ">= 3.6" + } + } +} + +=== variables.tf === +variable "resource_prefix" { + type = string + description = "Optional stable physical resource prefix. Leave empty to generate one." + default = "" + + validation { + condition = var.resource_prefix == "" || (can(regex("^[a-z][a-z0-9-]{1,38}[a-z0-9]$", var.resource_prefix)) && length(regexall("--", var.resource_prefix)) == 0) + error_message = "resource_prefix must be 3-40 characters: lowercase letters, numbers, and hyphens; start with a letter; end with a letter or number; and not contain consecutive hyphens." + } +} + +variable "name" { + type = string + description = "Human-readable application name shown in setup and cloud IAM review metadata." +} + +variable "token" { + type = string + description = "Install token from the application setup page. This is the same token used by the deploy CLI --token flag." + sensitive = true +} + +variable "management_url" { + type = string + description = "Optional management endpoint used by pull-style runtimes." + default = "" +} + +variable "deployment_model" { + type = string + description = "How runtime updates are delivered after setup." + default = "push" + + validation { + condition = contains(["push", "pull"], var.deployment_model) + error_message = "deployment_model must be one of: push, pull." + } +} + +variable "advanced_settings_json" { + type = string + description = "Advanced JSON-encoded deployment settings. Most installations should use the typed variables in this module instead." + default = "{}" + sensitive = true +} + +variable "advanced_settings_overlay_json" { + type = string + description = "JSON-encoded deployment settings merged over the package defaults. Use this for partial advanced-setting overrides that must preserve generated defaults such as compute selections." + default = "{}" + sensitive = true +} + +variable "updates_mode" { + type = string + description = "How application updates are delivered after setup." + default = "auto" + + validation { + condition = contains(["auto", "approval-required"], var.updates_mode) + error_message = "updates_mode must be one of: auto, approval-required." + } +} + +variable "telemetry_mode" { + type = string + description = "How logs, metrics, and traces are collected." + default = "auto" + + validation { + condition = contains(["off", "auto", "approval-required"], var.telemetry_mode) + error_message = "telemetry_mode must be one of: off, auto, approval-required." + } +} + +variable "heartbeats_mode" { + type = string + description = "Whether runtime health checks are enabled." + default = "on" + + validation { + condition = contains(["off", "on"], var.heartbeats_mode) + error_message = "heartbeats_mode must be one of: off, on." + } +} + +variable "azure_location" { + type = string + description = "Azure location." + default = "eastus" +} + +variable "azure_subscription_id" { + type = string + description = "Azure subscription ID hosting the stack." +} + +variable "azure_resource_group_name" { + type = string + description = "Azure resource group name." +} + +=== providers.tf === +provider "azurerm" { + resource_provider_registrations = "none" + + features {} +} + +=== resource_prefix.tf === +resource "random_id" "resource_prefix" { + byte_length = 4 +} + +=== locals.tf === +locals { + resource_prefix = var.resource_prefix == "" ? format("a%s", random_id.resource_prefix.hex) : var.resource_prefix + deployment_name = var.name + deployment_platform = "azure" + deployment_target = "azure" + deployment_region = var.azure_location + deployment_management_config = null + advanced_settings = merge(jsondecode(var.advanced_settings_json), jsondecode(var.advanced_settings_overlay_json)) + deployment_settings = merge(local.advanced_settings, { deploymentModel = var.deployment_model, updates = var.updates_mode, telemetry = var.telemetry_mode, heartbeats = var.heartbeats_mode }) + deployment_resources = [ + { + id = "default-resource-group" + type = "azure_resource_group" + importData = { + subscriptionId = var.azure_subscription_id + resourceGroup = azurerm_resource_group.default_resource_group.name + location = azurerm_resource_group.default_resource_group.location + } + }, + { + id = "llm" + type = "ai" + importData = { + subscriptionId = var.azure_subscription_id + resourceGroup = var.azure_resource_group_name + accountName = azurerm_cognitive_account.llm.name + endpoint = azurerm_cognitive_account.llm.endpoint + location = var.azure_location + } + } + ] +} + +=== default_resource_group.tf === +resource "azurerm_resource_group" "default_resource_group" { + name = var.azure_resource_group_name + location = var.azure_location + tags = { + "managed-by" = "setup" + deployment = local.resource_prefix + resource = "default-resource-group" + "resource-type" = "resource-group" + } +} + +=== llm.tf === +resource "azurerm_cognitive_account" "llm" { + name = "${local.resource_prefix}-llm" + resource_group_name = var.azure_resource_group_name + location = var.azure_location + kind = "AIServices" + sku_name = "S0" + custom_subdomain_name = "${local.resource_prefix}-llm" + tags = { + "managed-by" = "setup" + deployment = local.resource_prefix + resource = "llm" + "resource-type" = "ai" + } + depends_on = [ + azurerm_resource_group.default_resource_group + ] +} + +resource "azurerm_cognitive_deployment" "llm_gpt-4_1" { + name = "gpt-4.1" + cognitive_account_id = azurerm_cognitive_account.llm.id + + model { + format = "OpenAI" + name = "gpt-4.1" + version = "2025-04-14" + } + + sku { + name = "GlobalStandard" + capacity = 1 + } + + depends_on = [ + azurerm_resource_group.default_resource_group + ] +} + +resource "azurerm_cognitive_deployment" "llm_gpt-4o-mini" { + name = "gpt-4o-mini" + cognitive_account_id = azurerm_cognitive_account.llm.id + + model { + format = "OpenAI" + name = "gpt-4o-mini" + version = "2024-07-18" + } + + sku { + name = "GlobalStandard" + capacity = 1 + } + + depends_on = [ + azurerm_resource_group.default_resource_group + ] +} + +resource "azurerm_cognitive_deployment" "llm_model-router" { + name = "model-router" + cognitive_account_id = azurerm_cognitive_account.llm.id + + model { + format = "OpenAI" + name = "model-router" + version = "2025-11-18" + } + + sku { + name = "GlobalStandard" + capacity = 1 + } + + depends_on = [ + azurerm_resource_group.default_resource_group + ] +} + +=== registration.tf === +resource "terraform_data" "deployment_registration" { + input = { + platform = local.deployment_platform + token = var.token + name = var.name + resource_prefix = local.resource_prefix + setup_target = "" + setup_import_format_version = 1 + setup_fingerprint = "" + setup_fingerprint_version = 0 + management_url = var.management_url + management_config = local.deployment_management_config + stack_settings = local.deployment_settings + resources = local.deployment_resources + inputValues = {} + } + depends_on = [ + azurerm_resource_group.default_resource_group, + azurerm_cognitive_account.llm, + azurerm_cognitive_deployment.llm_gpt-4_1, + azurerm_cognitive_deployment.llm_gpt-4o-mini, + azurerm_cognitive_deployment.llm_model-router + ] +} + +=== outputs.tf === +output "deployment_target" { + value = "azure" + description = "Terraform module target." +} + +output "deployment_resource_prefix" { + value = local.resource_prefix + description = "Physical resource prefix." +} + +output "deployment_platform" { + value = local.deployment_platform + description = "Target platform." +} + +output "deployment_region" { + value = local.deployment_region + description = "Target cloud region or location." +} + +output "deployment_setup_target" { + value = "" + description = "Setup target." +} + +output "deployment_setup_import_format_version" { + value = 1 + description = "Setup registration payload format version." +} + +output "deployment_setup_fingerprint" { + value = "" + description = "Setup compatibility fingerprint." +} + +output "deployment_setup_fingerprint_version" { + value = 0 + description = "Setup fingerprint algorithm version." +} + +output "deployment_management_config" { + value = jsonencode(local.deployment_management_config) + description = "Deployment registration management configuration JSON." +} + +output "deployment_stack_settings" { + value = jsonencode(local.deployment_settings) + description = "Deployment registration settings JSON." + sensitive = true +} + +output "deployment_resources" { + value = jsonencode(local.deployment_resources) + description = "Deployment registration resource metadata JSON." +} + +=== README.md === +# Deployment setup - acme-ai + +Target: `azure`. + +This module creates setup-owned infrastructure, grants the management access needed after setup, and prepares deployment registration metadata. Review the generated `.tf` files before applying; each resource file maps to one setup resource. + +## Inputs + +Required: + +- `token`: install token from the setup page. +- `name`: deployment name to include in the registration metadata. + +Common optional settings: + +- `resource_prefix`: stable physical-name prefix. Leave empty to generate one. +- `management_url`: optional management endpoint used by pull-style runtimes. +- `deployment_model`: `push` or `pull`. +- `updates_mode`: `auto` or `approval-required`. +- `telemetry_mode`: `off`, `auto`, or `approval-required`. +- `heartbeats_mode`: `off` or `on`. +- `advanced_settings_json`: complete advanced deployment settings JSON. Most installs should keep the generated default. +- `advanced_settings_overlay_json`: partial advanced settings merged over package defaults, preserving generated values such as compute selections. + +Azure settings: + +- `azure_subscription_id`: target subscription ID. +- `azure_location`: Azure location. +- `azure_resource_group_name`: target resource group name. +- `azure_managing_tenant_id`, `azure_oidc_issuer`, `azure_oidc_subject`: management identity trust settings when this setup grants Azure management access. + +## Run + +Use your organization's normal backend and approval workflow. A typical local review looks like: + +```bash +export TF_VAR_name="acme-ai" +export TF_VAR_token="..." +terraform init +terraform validate +terraform plan -out=tfplan +terraform apply tfplan +``` + +## Registration + +This module exposes `deployment_management_config`, `deployment_stack_settings`, and `deployment_resources` outputs for registration flows managed outside Terraform. + +## Outputs + +- `deployment_management_config`: management endpoint and credential-boundary metadata. +- `deployment_stack_settings`: deployment settings JSON assembled from typed variables, package defaults, and advanced-setting overlays. +- `deployment_resources`: setup-owned resource metadata handed to the deployment runtime. +- `deployment_id` and `deployment_token`: emitted only when Terraform performs registration. diff --git a/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__gcp_ai_minimal.snap b/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__gcp_ai_minimal.snap new file mode 100644 index 000000000..4caf7cf11 --- /dev/null +++ b/crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__gcp_ai_minimal.snap @@ -0,0 +1,302 @@ +--- +source: crates/alien-terraform/tests/generator/helpers.rs +expression: buf +--- +=== versions.tf === +terraform { + required_version = ">= 1.5.0" + + required_providers { + google = { + source = "hashicorp/google" + version = ">= 5.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.6" + } + } +} + +=== variables.tf === +variable "resource_prefix" { + type = string + description = "Optional stable physical resource prefix. Leave empty to generate one." + default = "" + + validation { + condition = var.resource_prefix == "" || (can(regex("^[a-z][a-z0-9-]{1,38}[a-z0-9]$", var.resource_prefix)) && length(regexall("--", var.resource_prefix)) == 0) + error_message = "resource_prefix must be 3-40 characters: lowercase letters, numbers, and hyphens; start with a letter; end with a letter or number; and not contain consecutive hyphens." + } +} + +variable "name" { + type = string + description = "Human-readable application name shown in setup and cloud IAM review metadata." +} + +variable "token" { + type = string + description = "Install token from the application setup page. This is the same token used by the deploy CLI --token flag." + sensitive = true +} + +variable "management_url" { + type = string + description = "Optional management endpoint used by pull-style runtimes." + default = "" +} + +variable "deployment_model" { + type = string + description = "How runtime updates are delivered after setup." + default = "push" + + validation { + condition = contains(["push", "pull"], var.deployment_model) + error_message = "deployment_model must be one of: push, pull." + } +} + +variable "advanced_settings_json" { + type = string + description = "Advanced JSON-encoded deployment settings. Most installations should use the typed variables in this module instead." + default = "{}" + sensitive = true +} + +variable "advanced_settings_overlay_json" { + type = string + description = "JSON-encoded deployment settings merged over the package defaults. Use this for partial advanced-setting overrides that must preserve generated defaults such as compute selections." + default = "{}" + sensitive = true +} + +variable "updates_mode" { + type = string + description = "How application updates are delivered after setup." + default = "auto" + + validation { + condition = contains(["auto", "approval-required"], var.updates_mode) + error_message = "updates_mode must be one of: auto, approval-required." + } +} + +variable "telemetry_mode" { + type = string + description = "How logs, metrics, and traces are collected." + default = "auto" + + validation { + condition = contains(["off", "auto", "approval-required"], var.telemetry_mode) + error_message = "telemetry_mode must be one of: off, auto, approval-required." + } +} + +variable "heartbeats_mode" { + type = string + description = "Whether runtime health checks are enabled." + default = "on" + + validation { + condition = contains(["off", "on"], var.heartbeats_mode) + error_message = "heartbeats_mode must be one of: off, on." + } +} + +variable "gcp_project" { + type = string + description = "GCP project ID." +} + +variable "gcp_region" { + type = string + description = "GCP region." + default = "us-central1" +} + +variable "managing_service_account_email" { + type = string + description = "Email of the management service account allowed to impersonate setup-created identities. Empty disables the binding." + default = "" +} + +variable "gcp_manage_custom_roles" { + type = bool + description = "Whether this module creates the GCP project custom roles it binds. Set to false when those roles are managed outside this stack." + default = true +} + +variable "gcp_custom_role_prefix" { + type = string + description = "Prefix used for GCP project custom role IDs when gcp_manage_custom_roles is false. Empty uses resource_prefix." + default = "" +} + +=== providers.tf === +provider "google" { + project = var.gcp_project + region = var.gcp_region +} + +=== resource_prefix.tf === +resource "random_id" "resource_prefix" { + byte_length = 4 +} + +=== locals.tf === +locals { + resource_prefix = var.resource_prefix == "" ? format("a%s", random_id.resource_prefix.hex) : var.resource_prefix + deployment_name = var.name + gcp_custom_role_prefix = substr(replace(lower(var.gcp_custom_role_prefix == "" ? local.resource_prefix : var.gcp_custom_role_prefix), "-", "_"), 0, 18) + deployment_platform = "gcp" + deployment_target = "gcp" + deployment_region = var.gcp_region + deployment_management_config = null + advanced_settings = merge(jsondecode(var.advanced_settings_json), jsondecode(var.advanced_settings_overlay_json)) + deployment_settings = merge(local.advanced_settings, { deploymentModel = var.deployment_model, updates = var.updates_mode, telemetry = var.telemetry_mode, heartbeats = var.heartbeats_mode }) + deployment_resources = [ + { + id = "llm" + type = "ai" + importData = { + projectId = var.gcp_project + location = var.gcp_region + } + } + ] +} + +=== registration.tf === +resource "terraform_data" "deployment_registration" { + input = { + platform = local.deployment_platform + token = var.token + name = var.name + resource_prefix = local.resource_prefix + setup_target = "" + setup_import_format_version = 1 + setup_fingerprint = "" + setup_fingerprint_version = 0 + management_url = var.management_url + management_config = local.deployment_management_config + stack_settings = local.deployment_settings + resources = local.deployment_resources + inputValues = {} + } +} + +=== outputs.tf === +output "deployment_target" { + value = "gcp" + description = "Terraform module target." +} + +output "deployment_resource_prefix" { + value = local.resource_prefix + description = "Physical resource prefix." +} + +output "deployment_platform" { + value = local.deployment_platform + description = "Target platform." +} + +output "deployment_region" { + value = local.deployment_region + description = "Target cloud region or location." +} + +output "deployment_setup_target" { + value = "" + description = "Setup target." +} + +output "deployment_setup_import_format_version" { + value = 1 + description = "Setup registration payload format version." +} + +output "deployment_setup_fingerprint" { + value = "" + description = "Setup compatibility fingerprint." +} + +output "deployment_setup_fingerprint_version" { + value = 0 + description = "Setup fingerprint algorithm version." +} + +output "deployment_management_config" { + value = jsonencode(local.deployment_management_config) + description = "Deployment registration management configuration JSON." +} + +output "deployment_stack_settings" { + value = jsonencode(local.deployment_settings) + description = "Deployment registration settings JSON." + sensitive = true +} + +output "deployment_resources" { + value = jsonencode(local.deployment_resources) + description = "Deployment registration resource metadata JSON." +} + +=== README.md === +# Deployment setup - acme-ai + +Target: `gcp`. + +This module creates setup-owned infrastructure, grants the management access needed after setup, and prepares deployment registration metadata. Review the generated `.tf` files before applying; each resource file maps to one setup resource. + +## Inputs + +Required: + +- `token`: install token from the setup page. +- `name`: deployment name to include in the registration metadata. + +Common optional settings: + +- `resource_prefix`: stable physical-name prefix. Leave empty to generate one. +- `management_url`: optional management endpoint used by pull-style runtimes. +- `deployment_model`: `push` or `pull`. +- `updates_mode`: `auto` or `approval-required`. +- `telemetry_mode`: `off`, `auto`, or `approval-required`. +- `heartbeats_mode`: `off` or `on`. +- `advanced_settings_json`: complete advanced deployment settings JSON. Most installs should keep the generated default. +- `advanced_settings_overlay_json`: partial advanced settings merged over package defaults, preserving generated values such as compute selections. + +GCP settings: + +- `gcp_project`: target GCP project ID. +- `gcp_region`: target GCP region. +- `managing_service_account_email`: management service account allowed to impersonate setup-created identities. +- `gcp_manage_custom_roles`: whether this module creates project custom roles. +- `gcp_custom_role_prefix`: custom role ID prefix when roles are managed outside this module. + +## Run + +Use your organization's normal backend and approval workflow. A typical local review looks like: + +```bash +export TF_VAR_name="acme-ai" +export TF_VAR_token="..." +terraform init +terraform validate +terraform plan -out=tfplan +terraform apply tfplan +``` + +## Registration + +This module exposes `deployment_management_config`, `deployment_stack_settings`, and `deployment_resources` outputs for registration flows managed outside Terraform. + +## Outputs + +- `deployment_management_config`: management endpoint and credential-boundary metadata. +- `deployment_stack_settings`: deployment settings JSON assembled from typed variables, package defaults, and advanced-setting overlays. +- `deployment_resources`: setup-owned resource metadata handed to the deployment runtime. +- `deployment_id` and `deployment_token`: emitted only when Terraform performs registration. diff --git a/crates/alien-test/src/e2e.rs b/crates/alien-test/src/e2e.rs index af7ae7de5..64a578d2e 100644 --- a/crates/alien-test/src/e2e.rs +++ b/crates/alien-test/src/e2e.rs @@ -206,6 +206,8 @@ pub enum Binding { ArtifactRegistry, /// Service account identity and impersonation ServiceAccount, + /// AI Gateway binding injection + a real model invoke (skippable via ALIEN_E2E_AI_SKIP_INVOKE=1) + Ai, } impl std::fmt::Display for Binding { @@ -231,6 +233,7 @@ impl std::fmt::Display for Binding { Binding::Build => write!(f, "build"), Binding::ArtifactRegistry => write!(f, "artifact-registry"), Binding::ServiceAccount => write!(f, "service-account"), + Binding::Ai => write!(f, "ai"), } } } @@ -272,6 +275,11 @@ pub fn supported_bindings(platform: Platform, model: DeploymentModel) -> Vec { bindings.push(Binding::Kv); @@ -346,6 +354,9 @@ pub fn exclusion_reason( { Some("Bun-on-Windows runtime issue: detached async tasks in waitUntil don't execute") } + Binding::Ai if app == TestApp::ComprehensiveRust => { + Some("AI binding check not implemented in the Rust test app") + } _ => None, } } diff --git a/crates/alien-test/tests/common/bindings.rs b/crates/alien-test/tests/common/bindings.rs index 3d22132d4..906eb70d6 100644 --- a/crates/alien-test/tests/common/bindings.rs +++ b/crates/alien-test/tests/common/bindings.rs @@ -15,6 +15,7 @@ const KV_BINDING: &str = "alien-kv"; const VAULT_BINDING: &str = "alien-vault"; const POSTGRES_BINDING: &str = "alien-postgres"; const QUEUE_BINDING: &str = "alien-queue"; +const AI_BINDING: &str = "test-ai"; /// Standard response shape returned by binding test endpoints. #[derive(Debug, Deserialize)] @@ -928,3 +929,114 @@ pub async fn check_service_account(deployment: &TestDeployment) -> anyhow::Resul info!("Service account binding check passed"); Ok(()) } + +// --------------------------------------------------------------------------- +// AI Gateway +// --------------------------------------------------------------------------- + +/// Response from the /ai-test endpoint. `model_count`, `model`, and `reply` are +/// present only on `?invoke=1` responses. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AiTestResponse { + injected: bool, + service: String, + locator: Option, + model_count: Option, + model: Option, + reply: Option, +} + +/// Check the AI binding end to end: GET /ai-test, then GET /ai-test?invoke=1. +/// +/// The first call proves the runtime injected ALIEN_TEST_AI_BINDING and that it +/// parsed to a well-formed bedrock config. The second lists the model catalog and +/// makes a real one-line chat call through the embedded gateway under the +/// workload's ambient credentials — proving the full app -> gateway -> cloud LLM +/// path, not just provisioning. The invoke half needs nonzero Bedrock on-demand +/// quota in the deployment's account; set ALIEN_E2E_AI_SKIP_INVOKE=1 to run +/// provisioning-only in a quota-zeroed account. +pub async fn check_ai(deployment: &TestDeployment) -> anyhow::Result<()> { + let url = deployment_url(deployment)?; + info!(binding = AI_BINDING, "Checking AI binding injection"); + + let resp = reqwest::Client::new() + .get(format!("{}/ai-test", url)) + .send() + .await + .context("AI test request failed")?; + + let status = resp.status(); + if !status.is_success() { + let body = resp.text().await.unwrap_or_default(); + bail!("AI test returned {}: {}", status, body); + } + + let data: AiTestResponse = resp + .json() + .await + .context("Failed to parse AI test response")?; + + if !data.injected { + bail!( + "AI binding not injected: ALIEN_TEST_AI_BINDING was missing or unparseable. \ + Response: {:?}", + data + ); + } + let expected_service = match deployment.platform.as_str() { + "aws" => "bedrock", + "gcp" => "vertex", + "azure" => "foundry", + other => bail!("AI binding check has no expected service for platform '{other}'"), + }; + if data.service != expected_service { + bail!( + "AI binding service mismatch: expected '{}', got '{}'.", + expected_service, + data.service + ); + } + let locator = data.locator.as_deref().unwrap_or(""); + if locator.is_empty() { + bail!("AI binding locator is empty; the controller must fill the service scope (region / project / endpoint)"); + } + + info!(service = %data.service, %locator, "AI binding injection check passed"); + + if std::env::var("ALIEN_E2E_AI_SKIP_INVOKE").as_deref() == Ok("1") { + info!("Skipping the real model invoke (ALIEN_E2E_AI_SKIP_INVOKE=1)"); + return Ok(()); + } + + let resp = reqwest::Client::new() + .get(format!("{}/ai-test?invoke=1", url)) + .timeout(std::time::Duration::from_secs(120)) + .send() + .await + .context("AI invoke request failed")?; + let status = resp.status(); + if !status.is_success() { + let body = resp.text().await.unwrap_or_default(); + bail!("AI invoke returned {}: {}", status, body); + } + let data: AiTestResponse = resp + .json() + .await + .context("Failed to parse AI invoke response")?; + let model_count = data.model_count.unwrap_or(0); + if model_count == 0 { + bail!("AI model catalog is empty; expected at least one curated model"); + } + let reply = data.reply.as_deref().unwrap_or(""); + if !reply.to_ascii_lowercase().contains("pong") { + bail!( + "AI chat reply did not contain 'pong': model={:?} reply={:?}", + data.model, + reply + ); + } + + info!(model = %data.model.as_deref().unwrap_or(""), %reply, "AI model invoke check passed"); + Ok(()) +} diff --git a/crates/alien-test/tests/common/runner.rs b/crates/alien-test/tests/common/runner.rs index d0282f6b1..816c5e040 100644 --- a/crates/alien-test/tests/common/runner.rs +++ b/crates/alien-test/tests/common/runner.rs @@ -61,6 +61,7 @@ pub async fn check_all_bindings( Binding::Build => bindings::check_build(deployment).await?, Binding::ArtifactRegistry => bindings::check_artifact_registry(deployment).await?, Binding::ServiceAccount => bindings::check_service_account(deployment).await?, + Binding::Ai => bindings::check_ai(deployment).await?, } info!(binding = %binding, "Binding check passed"); diff --git a/examples/README.md b/examples/README.md index 76572caf3..902ae4847 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,6 +4,8 @@ Each example is a self-contained template you can initialize with `alien init`. | Template | Description | Language | |----------|-------------|----------| +| [ai-quickstart-ts](./ai-quickstart-ts) | The smallest AI setup: one worker asking cloud LLMs questions through the embedded AI gateway (no API keys). | TypeScript | +| [ai-finetune-inference-ts](./ai-finetune-inference-ts) | Fine-tune a base model in the customer's cloud and serve it for inference — data and weights never leave the account. | TypeScript | | [remote-worker-ts](./remote-worker-ts) | Execute tool calls in your customer's cloud. The AI worker pattern. | TypeScript | | [basic-worker-ts](./basic-worker-ts) | The simplest Alien worker, in TypeScript. | TypeScript | | [basic-worker-rs](./basic-worker-rs) | The simplest Alien worker, in Rust. | Rust | diff --git a/examples/ai-finetune-inference-ts/README.md b/examples/ai-finetune-inference-ts/README.md new file mode 100644 index 000000000..9a23e1c68 --- /dev/null +++ b/examples/ai-finetune-inference-ts/README.md @@ -0,0 +1,100 @@ +# AI Fine-tune + Inference + +Fine-tune a base model **inside the customer's cloud**, then serve it for inference — the training data and the tuned weights never leave the customer's account. Extends the AI gateway (`alien.AI`) with a `.finetune()` declaration. + +## What it shows + +- One `alien.AI` resource that both **tunes** a base model and **serves** it (plus the base foundation models) through the same OpenAI-compatible gateway. +- Training data read from the customer's own object storage (S3 / GCS / Blob) under the workload's ambient identity — no keys, no data egress. +- The same app deploys unchanged to **AWS Bedrock**, **GCP Vertex AI**, and **Azure AI Foundry**; the resource resolves to the deploy-target's managed tuning + inference service. + +## How it works + +Fine-tuning is triggered **at runtime by the app**, not at deploy time. The +`alien.AI("llm")` resource provisions and is **Ready immediately** — it's just the +inference gateway plus a fine-tuning capability. The app starts a job by calling the +gateway; the resource never blocks on a hours-long training job. + +``` +deploy: alien.Storage("finetune-training-data") # S3 / GCS / Blob, customer's account + alien.AI("llm").finetune({...}) # Ready immediately — capability only + # (no job runs at deploy) + +runtime: POST /dataset → upload training.jsonl into the bucket + POST /finetune → ai("llm").finetune() submits the cloud job → { jobId } + GET /finetune/status → ai("llm").finetuneStatus(jobId) → running|succeeded|failed + POST /chat-tuned → inference on "support-tuned" once the job succeeded +``` + +When the app calls `ai("llm").finetune(...)`, the gateway submits the provider's tuning +job (Bedrock `CreateModelCustomizationJob`, Vertex `tuningJobs`, or Foundry +`fine_tuning.jobs`) under the workload's ambient identity, reading the dataset from the +customer's bucket, and returns a job id. Inference for `support-tuned` works once the job +succeeds: the gateway **rediscovers** the completed tuned model by convention (no stored +state) and routes to it — so app code calls it exactly like a base model, only the `model` +string differs. + +## API + +| Route | Method | Purpose | +|-------|--------|---------| +| `/dataset` | POST | Upload JSONL training data into the customer's bucket (body = JSONL) | +| `/finetune` | POST | Start a tuning job at runtime; returns `{ jobId, servedModel }` | +| `/finetune/status` | GET | Poll a job: `?jobId=` → `{ status, model?, message? }` | +| `/chat` | POST | Inference against a base foundation model (`{ "message": "..." }`) | +| `/chat-tuned` | POST | Inference against the fine-tuned model — same call, different model id | + +## Run locally + +```bash +npm install +alien dev +``` + +Upload data, start a job, poll it, then query the tuned model: + +```bash +curl -X POST --data-binary @sample-training.jsonl http://localhost:8080/dataset +JOB=$(curl -s -X POST http://localhost:8080/finetune | jq -r .jobId) +curl "http://localhost:8080/finetune/status?jobId=$JOB" +# once succeeded: +curl -X POST http://localhost:8080/chat-tuned -d '{"message":"How do I reset my password?"}' +``` + +On the **local** platform the AI resource is a BYO-key provider (set `OPENAI_API_KEY`); +fine-tuning is a managed-cloud capability, so `POST /finetune` is rejected locally and the +tuned route falls back to the base model. Deploy to a cloud to exercise the real tuning flow. + +## Picking `baseModel` per cloud + +`baseModel` is a **provider-native** id — set it to match the cloud you deploy to (see `alien.ts`): + +| Cloud | Service | Example `baseModel` | Tuning method | +|-------|---------|--------------------|---------------| +| AWS | Bedrock | `amazon.nova-lite-v1:0` | SFT (`sft`) — also RFT on Nova | +| GCP | Vertex AI | a Gemini model id | Supervised (`sft`); Vertex does **not** expose LoRA/QLoRA as a user knob for Gemini | +| Azure | AI Foundry | a `gpt-4o` / `gpt-4.1` family id | `sft`, plus `dpo` on some models (LoRA underneath) | + +## Data residency — read before you ship + +"In the customer's cloud" is not automatic on every tier. What the verified provider docs say: + +- **AWS Bedrock** — training data is S3-in / S3-out in the customer's buckets; the job runs under a customer IAM role; the custom model serves **on-demand** (Provisioned Throughput is *not* required — a common misconception). Private connectivity via PrivateLink. Region-confined by default. +- **Azure AI Foundry** — training data and the tuned model are stored at rest in the customer's Foundry resource, in-tenant, same geography (AES-256, optional CMK). **But**: the *Global Standard* and *Developer* deployment/training tiers may move weights outside the resource's region for cost — pin **Standard** if you need strict residency. Two gotchas: training JSONL must be UTF-8 **with a BOM**, and importing from Blob requires the storage account to allow **public** network access. +- **GCP Vertex AI** — supervised-tunes Gemini and auto-provisions a managed tuned-model endpoint; per-epoch checkpoints are auto-deployed. Residency follows the chosen region, but auto-deployed checkpoints can relax strict regional confinement — verify for your region. + +For a hard "training data **and** tuned weights never leave the chosen region" guarantee, pin the region and the residency-preserving tier on each provider (Bedrock in-region + Standard-equivalent, Azure **Standard**, Vertex single-region), and confirm against current provider docs — these tiers and defaults change. + +## Training data format + +JSONL, one example per line, in the OpenAI chat/conversational shape: + +```json +{"messages":[{"role":"user","content":"How do I reset my password?"},{"role":"assistant","content":"Go to Settings → Security → Reset password, then follow the emailed link."}]} +``` + +See `sample-training.jsonl`. Real fine-tuning needs enough examples to matter (Azure requires ≥ 10; providers recommend hundreds) — the sample is illustrative. + +## License + +ISC diff --git a/examples/ai-finetune-inference-ts/alien.ts b/examples/ai-finetune-inference-ts/alien.ts new file mode 100644 index 000000000..7e23128fb --- /dev/null +++ b/examples/ai-finetune-inference-ts/alien.ts @@ -0,0 +1,64 @@ +// EXAMPLE: Fine-tune a base model in the customer's cloud, then serve it for +// inference — without the training data or the tuned weights ever leaving the +// customer's account. + +import * as alien from "@alienplatform/core" + +// The training dataset lives in the customer's object storage: +// S3 on AWS, Cloud Storage on GCP, Blob Storage on Azure. The worker uploads +// JSONL examples here; the tuning job reads them in-account. +// +// NOTE: S3 bucket names are GLOBALLY unique across all AWS accounts. The bucket +// is named `-`, so a generic id like "dataset" +// can collide with a bucket someone else already owns. Keep this id distinctive +// (and change it if you hit "bucket name is not available"). +const dataset = new alien.Storage("finetune-training-data").build() + +// A model-less AI gateway with a fine-tuning CAPABILITY. `.finetune(...)` here is a +// declaration, not a deploy-time trigger: the resource provisions and is Ready +// immediately (no job runs at deploy). The app starts a tuning job at RUNTIME by +// calling `ai("llm").finetune(...)` (see src/index.ts) — the gateway then submits the +// provider's job (Bedrock CreateModelCustomizationJob, Vertex tuningJobs, or Foundry +// fine_tuning.jobs) reading `dataset` in the customer's account, and serves the tuned +// model under `servedModelId` once it completes. Base models remain callable too. +// +// `baseModel` is a provider-native id; pick the one that matches the cloud you +// deploy to (see the README's per-provider table). The default here targets +// AWS Bedrock (Amazon Nova). +const llm = new alien.AI("llm") + .finetune({ + baseModel: "amazon.nova-lite-v1:0", + trainingData: dataset, + trainingKey: "training.jsonl", + servedModelId: "support-tuned", + method: "sft", + }) + .build() + +const api = new alien.Worker("api") + .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) + // GCP Cloud Run gen2 requires >= 512 MiB; the default 256 MiB fails its preflight. + .memoryMb(512) + .publicEndpoint("api") + // Linking injects the dataset binding and the AI gateway (ALIEN_LLM_BINDING). + .link(dataset) + .link(llm) + .permissions("execution") + .build() + +export default new alien.Stack("ai-finetune-inference") + .platforms(["aws", "gcp", "azure"]) + .add(dataset, "live") + .add(llm, "live") + .add(api, "live") + .permissions({ + profiles: { + execution: { + // Read/write the dataset bucket, and both submit the tuning job and + // invoke models (base + tuned) through the gateway. + "finetune-training-data": ["storage/data-read", "storage/data-write"], + "*": ["ai/invoke", "ai/finetune"], + }, + }, + }) + .build() diff --git a/examples/ai-finetune-inference-ts/package.json b/examples/ai-finetune-inference-ts/package.json new file mode 100644 index 000000000..a0e540782 --- /dev/null +++ b/examples/ai-finetune-inference-ts/package.json @@ -0,0 +1,22 @@ +{ + "name": "ai-finetune-inference-ts", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "alien dev", + "build": "alien build --platform local", + "test": "vitest run" + }, + "dependencies": { + "@alienplatform/sdk": "^1.8.0", + "@alienplatform/core": "^1.8.0", + "hono": "^4.0.0" + }, + "devDependencies": { + "@alienplatform/testing": "^1.8.0", + "@types/node": "^24.0.15", + "typescript": "^5.8.3", + "vitest": "^3.2.7" + } +} diff --git a/examples/ai-finetune-inference-ts/sample-training.jsonl b/examples/ai-finetune-inference-ts/sample-training.jsonl new file mode 100644 index 000000000..33545a4d1 --- /dev/null +++ b/examples/ai-finetune-inference-ts/sample-training.jsonl @@ -0,0 +1,10 @@ +{"messages":[{"role":"user","content":"How do I reset my password?"},{"role":"assistant","content":"Go to Settings → Security → Reset password, then follow the emailed link. The link expires in 30 minutes."}]} +{"messages":[{"role":"user","content":"My invoice looks wrong this month."},{"role":"assistant","content":"I can help. Open Billing → Invoices, select the invoice, and click 'Report an issue'. Include the line item you're disputing and our team responds within one business day."}]} +{"messages":[{"role":"user","content":"How do I invite a teammate?"},{"role":"assistant","content":"Open Team → Members → Invite, enter their work email, pick a role, and send. They'll get an email to join your workspace."}]} +{"messages":[{"role":"user","content":"Can I export my data?"},{"role":"assistant","content":"Yes. Go to Settings → Data → Export, choose CSV or JSON, and we'll email a download link when it's ready — usually within a few minutes."}]} +{"messages":[{"role":"user","content":"The app is slow today."},{"role":"assistant","content":"Sorry about that. First check status.example.com for incidents. If all green, try clearing your cache and reloading; if it persists, send us your account id and region and we'll investigate."}]} +{"messages":[{"role":"user","content":"How do I cancel my subscription?"},{"role":"assistant","content":"Open Billing → Plan → Cancel subscription. You keep access until the end of the current billing period, and no further charges are made."}]} +{"messages":[{"role":"user","content":"Do you support single sign-on?"},{"role":"assistant","content":"Yes, on Business and Enterprise plans. Go to Settings → Security → SSO to configure SAML or OIDC with your identity provider."}]} +{"messages":[{"role":"user","content":"I didn't get my verification email."},{"role":"assistant","content":"Check your spam folder first. If it's not there, request a new one from the login screen — 'Resend verification'. Allow a few minutes for delivery."}]} +{"messages":[{"role":"user","content":"How do I change my billing email?"},{"role":"assistant","content":"Open Billing → Settings → Billing contact, update the email, and save. Future invoices go to the new address."}]} +{"messages":[{"role":"user","content":"Is my data encrypted?"},{"role":"assistant","content":"Yes — data is encrypted in transit (TLS) and at rest (AES-256). Enterprise plans can bring their own encryption key."}]} diff --git a/examples/ai-finetune-inference-ts/src/index.ts b/examples/ai-finetune-inference-ts/src/index.ts new file mode 100644 index 000000000..4c46b0879 --- /dev/null +++ b/examples/ai-finetune-inference-ts/src/index.ts @@ -0,0 +1,74 @@ +import { ai, storage } from "@alienplatform/sdk" +import { Hono } from "hono" + +// The public id the tuned model is served under (matches `servedModelId` in alien.ts). +const TUNED_MODEL = "support-tuned" +const TRAINING_KEY = "training.jsonl" + +const app = new Hono() + +// 1. Upload the JSONL training set into the customer's bucket. Nothing is tuned yet — +// the AI resource is already provisioned and Ready; fine-tuning is triggered at +// runtime (step 2). The data never leaves the customer's cloud. +app.post("/dataset", async c => { + const body = await c.req.text() + if (!body.trim()) { + return c.json({ error: "POST JSONL training data as the request body" }, 400) + } + await storage("finetune-training-data").put(TRAINING_KEY, new TextEncoder().encode(body)) + const lines = body.split("\n").filter(l => l.trim()).length + return c.json({ uploaded: TRAINING_KEY, examples: lines }) +}) + +// 2. Trigger fine-tuning at runtime. This is the whole point: the provisioned `llm` +// resource is always Ready, and the app itself kicks off a job by calling the +// gateway — `ai("llm").finetune(...)`. The gateway submits the cloud tuning job +// (Bedrock / Vertex / Foundry) under the workload's ambient identity and returns a +// job id to poll. Long-running (minutes to hours); this returns immediately. +app.post("/finetune", async c => { + const { jobId, servedModel } = await ai("llm").finetune({ trainingKey: TRAINING_KEY }) + return c.json({ jobId, servedModel, message: "tuning started; poll /finetune/status?jobId=" }) +}) + +// 3. Poll a job. The gateway queries the cloud live (stateless — no job state stored). +app.get("/finetune/status", async c => { + const jobId = c.req.query("jobId") + if (!jobId) { + return c.json({ error: "pass ?jobId= from the POST /finetune response" }, 400) + } + const state = await ai("llm").finetuneStatus(jobId) + return c.json(state) +}) + +// 4. Inference against a base foundation model (the per-cloud catalog). +app.post("/chat", async c => { + return chat(c, await defaultBaseModel()) +}) + +// 5. Inference against the fine-tuned model — same OpenAI-compatible call, just a +// different `model` id. Works once the job has succeeded: the gateway rediscovers +// the completed tuned model by convention and routes to it. +app.post("/chat-tuned", async c => { + return chat(c, TUNED_MODEL) +}) + +async function defaultBaseModel(): Promise { + const models = await ai("llm").getAvailableModels() + const base = models.find(m => m.id !== TUNED_MODEL)?.id + if (!base) throw new Error("no base models available for this cloud") + return base +} + +async function chat(c: Parameters[1]>[0], model: string) { + const { message } = await c.req.json<{ message?: string }>() + if (!message) { + return c.json({ error: "send { \"message\": \"...\" }" }, 400) + } + const completion = (await ai("llm").chat.completions.create({ + model, + messages: [{ role: "user", content: message }], + })) as { choices?: Array<{ message?: { content?: string } }> } + return c.json({ model, answer: completion.choices?.[0]?.message?.content ?? "" }) +} + +export default app diff --git a/examples/ai-finetune-inference-ts/template.toml b/examples/ai-finetune-inference-ts/template.toml new file mode 100644 index 000000000..8573c7917 --- /dev/null +++ b/examples/ai-finetune-inference-ts/template.toml @@ -0,0 +1,3 @@ +name = "ai-finetune-inference-ts" +description = "Fine-tune a base model in the customer's cloud and serve it for inference through the embedded Alien AI gateway — training data and tuned weights never leave the account." +language = "TypeScript" diff --git a/examples/ai-finetune-inference-ts/tests/ai-finetune-inference.test.ts b/examples/ai-finetune-inference-ts/tests/ai-finetune-inference.test.ts new file mode 100644 index 000000000..638211f5e --- /dev/null +++ b/examples/ai-finetune-inference-ts/tests/ai-finetune-inference.test.ts @@ -0,0 +1,68 @@ +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { type Deployment, deploy } from "@alienplatform/testing" +import { afterAll, beforeAll, describe, expect, it } from "vitest" + +// The local platform serves the AI gateway as a BYO-key provider; fine-tuning is +// a managed-cloud capability, so locally we verify the deployable surface — +// dataset upload, status shape, and (when a key is present) base inference — not +// a real tuning job. The cloud tuning flow is exercised by deploying to a cloud. +const OPENAI_KEY = process.env.OPENAI_API_KEY + +describe("ai-finetune-inference-ts", () => { + let deployment: Deployment + + beforeAll(async () => { + deployment = await deploy({ app: ".", platform: "local" }) + }, 300_000) + + afterAll(async () => { + await deployment?.destroy() + }) + + it("uploads training data into the customer bucket", async () => { + const jsonl = readFileSync( + fileURLToPath(new URL("../sample-training.jsonl", import.meta.url)), + "utf8", + ) + const response = await fetch(`${deployment.url}/dataset`, { + method: "POST", + body: jsonl, + }) + expect(response.status).toBe(200) + const body = (await response.json()) as { uploaded: string; examples: number } + expect(body.uploaded).toBe("training.jsonl") + expect(body.examples).toBe(10) + }) + + it("rejects an empty dataset upload", async () => { + const response = await fetch(`${deployment.url}/dataset`, { method: "POST", body: "" }) + expect(response.status).toBe(400) + }) + + it("requires a jobId to poll status", async () => { + // /finetune/status needs the jobId from a POST /finetune response. + const response = await fetch(`${deployment.url}/finetune/status`) + expect(response.status).toBe(400) + }) + + it("rejects starting a job on the local (BYO-key) platform", async () => { + // Fine-tuning is a managed-cloud capability; the local platform serves the AI + // resource as a BYO-key provider, so triggering a job is not supported here. + // (On a real cloud deploy this returns { jobId, servedModel }.) + const response = await fetch(`${deployment.url}/finetune`, { method: "POST" }) + expect(response.status).toBeGreaterThanOrEqual(400) + }) + + it.skipIf(!OPENAI_KEY)("answers a base-model chat when a provider key is set", async () => { + const response = await fetch(`${deployment.url}/chat`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ message: "Say the single word: pong" }), + }) + expect(response.status).toBe(200) + const body = (await response.json()) as { model: string; answer: string } + expect(body.model).toBeTruthy() + expect(body.answer.length).toBeGreaterThan(0) + }) +}) diff --git a/examples/ai-finetune-inference-ts/tsconfig.json b/examples/ai-finetune-inference-ts/tsconfig.json new file mode 100644 index 000000000..c6051cab0 --- /dev/null +++ b/examples/ai-finetune-inference-ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*", "alien.ts", "tests/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/ai-finetune-inference-ts/vitest.config.ts b/examples/ai-finetune-inference-ts/vitest.config.ts new file mode 100644 index 000000000..5dd3c9e50 --- /dev/null +++ b/examples/ai-finetune-inference-ts/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + testTimeout: 300_000, // 5 min timeout for tests involving deployment + hookTimeout: 300_000, // 5 min for beforeAll/afterAll + pool: "forks", // Use forks to ensure clean process state + }, +}) diff --git a/examples/ai-quickstart-ts/README.md b/examples/ai-quickstart-ts/README.md new file mode 100644 index 000000000..0b6f3a74b --- /dev/null +++ b/examples/ai-quickstart-ts/README.md @@ -0,0 +1,25 @@ +# AI quickstart (TypeScript) + +The smallest possible Alien AI setup: one worker, one `AI` resource, no database. +The worker asks a question and the embedded gateway forwards it to a model served +by the deployment's own cloud — Bedrock on AWS, Vertex on GCP, Azure AI Foundry on +Azure — under the workload's ambient identity. No API keys anywhere. + +## Run it locally + +```bash +OPENAI_API_KEY=sk-... alien dev +``` + +Locally there is no cloud identity, so the SDK uses your key directly (a +BYO-key binding) instead of the gateway. + +## Try it + +```bash +curl "$URL/models" +curl "$URL/ask?q=Reply+with+exactly+one+word:+pong" +``` + +`/models` lists the curated models for the cloud you deployed to; `/ask` picks +the first one unless you pass `?model=`. diff --git a/examples/ai-quickstart-ts/alien.ts b/examples/ai-quickstart-ts/alien.ts new file mode 100644 index 000000000..571ec8080 --- /dev/null +++ b/examples/ai-quickstart-ts/alien.ts @@ -0,0 +1,26 @@ +import * as alien from "@alienplatform/core" + +// A model-less AI resource. The customer's cloud serves the inference; the +// embedded gateway injects the workload's ambient identity, so no API keys. +const llm = new alien.AI("llm").build() + +const api = new alien.Worker("api") + .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) + .publicEndpoint("api") + // Linking injects ALIEN_LLM_BINDING and exposes the gateway as ALIEN_AI_GATEWAY_URL. + .link(llm) + .permissions("execution") + .build() + +export default new alien.Stack("ai-quickstart") + .platforms(["aws", "gcp", "azure"]) + .add(llm, "live") + .add(api, "live") + .permissions({ + profiles: { + execution: { + "*": ["ai/invoke"], + }, + }, + }) + .build() diff --git a/examples/ai-quickstart-ts/package.json b/examples/ai-quickstart-ts/package.json new file mode 100644 index 000000000..82bc895d6 --- /dev/null +++ b/examples/ai-quickstart-ts/package.json @@ -0,0 +1,19 @@ +{ + "name": "ai-quickstart-ts", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "alien dev", + "build": "alien build --platform local" + }, + "dependencies": { + "@alienplatform/sdk": "^1.8.0", + "@alienplatform/core": "^1.8.0", + "hono": "^4.0.0" + }, + "devDependencies": { + "@types/node": "^24.0.15", + "typescript": "^5.8.3" + } +} diff --git a/examples/ai-quickstart-ts/src/index.ts b/examples/ai-quickstart-ts/src/index.ts new file mode 100644 index 000000000..ba2fa3bd5 --- /dev/null +++ b/examples/ai-quickstart-ts/src/index.ts @@ -0,0 +1,30 @@ +import { ai } from "@alienplatform/sdk" +import { Hono } from "hono" + +const app = new Hono() + +// The models this stack can call on the current cloud (curated per-cloud catalog). +app.get("/models", async c => { + const models = await ai("llm").getAvailableModels() + return c.json({ models: models.map(m => m.id) }) +}) + +// One-shot question -> answer. `?model=` overrides the default (first catalog entry). +app.get("/ask", async c => { + const question = c.req.query("q") + if (!question) { + return c.json({ error: "pass a question as ?q=..." }, 400) + } + const llm = ai("llm") + const model = c.req.query("model") ?? (await llm.getAvailableModels())[0]?.id + if (!model) { + return c.json({ error: "no models available for this cloud" }, 500) + } + const completion = (await llm.chat.completions.create({ + model, + messages: [{ role: "user", content: question }], + })) as { choices?: Array<{ message?: { content?: string } }> } + return c.json({ model, answer: completion.choices?.[0]?.message?.content ?? "" }) +}) + +export default app diff --git a/examples/ai-quickstart-ts/template.toml b/examples/ai-quickstart-ts/template.toml new file mode 100644 index 000000000..eaf3985ba --- /dev/null +++ b/examples/ai-quickstart-ts/template.toml @@ -0,0 +1,3 @@ +name = "ai-quickstart-ts" +description = "The smallest AI setup: one worker asking cloud LLMs questions through the embedded Alien AI gateway (no API keys, no database)." +language = "TypeScript" diff --git a/examples/ai-quickstart-ts/tsconfig.json b/examples/ai-quickstart-ts/tsconfig.json new file mode 100644 index 000000000..929b8ae41 --- /dev/null +++ b/examples/ai-quickstart-ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*", "alien.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/package.json b/examples/package.json index 8fd14e67b..fd0f8e556 100644 --- a/examples/package.json +++ b/examples/package.json @@ -13,7 +13,8 @@ "@alienplatform/sdk": "file:../packages/sdk", "@alienplatform/testing": "file:../packages/testing", "@alienplatform/commands": "file:../packages/commands", - "@alienplatform/bindings": "file:../packages/bindings" + "@alienplatform/bindings": "file:../packages/bindings", + "@alienplatform/ai-gateway": "file:../packages/ai-gateway" } } } diff --git a/examples/pnpm-lock.yaml b/examples/pnpm-lock.yaml index c69906419..d0efa4199 100644 --- a/examples/pnpm-lock.yaml +++ b/examples/pnpm-lock.yaml @@ -11,11 +11,56 @@ overrides: '@alienplatform/testing': file:../packages/testing '@alienplatform/commands': file:../packages/commands '@alienplatform/bindings': file:../packages/bindings + '@alienplatform/ai-gateway': file:../packages/ai-gateway importers: .: {} + ai-finetune-inference-ts: + dependencies: + '@alienplatform/core': + specifier: file:../../packages/core + version: file:../packages/core(@types/json-schema@7.0.15)(openapi-types@12.1.3) + '@alienplatform/sdk': + specifier: file:../../packages/sdk + version: file:../packages/sdk(@types/json-schema@7.0.15)(openapi-types@12.1.3) + hono: + specifier: ^4.0.0 + version: 4.12.5 + devDependencies: + '@alienplatform/testing': + specifier: file:../../packages/testing + version: file:../packages/testing(@aws-sdk/client-ssm@3.1004.0)(@azure/identity@4.13.0)(@azure/keyvault-secrets@4.10.0(@azure/core-client@1.10.1))(@google-cloud/secret-manager@5.6.0)(@types/json-schema@7.0.15)(openapi-types@12.1.3) + '@types/node': + specifier: ^24.0.15 + version: 24.12.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.2.7 + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + + ai-quickstart-ts: + dependencies: + '@alienplatform/core': + specifier: file:../../packages/core + version: file:../packages/core(@types/json-schema@7.0.15)(openapi-types@12.1.3) + '@alienplatform/sdk': + specifier: file:../../packages/sdk + version: file:../packages/sdk(@types/json-schema@7.0.15)(openapi-types@12.1.3) + hono: + specifier: ^4.0.0 + version: 4.12.5 + devDependencies: + '@types/node': + specifier: ^24.0.15 + version: 24.12.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + basic-worker-rs: dependencies: '@alienplatform/core': @@ -33,7 +78,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) basic-worker-ts: dependencies: @@ -58,7 +103,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) byoc-database: devDependencies: @@ -76,7 +121,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.2.6 - version: 3.2.7(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0) command-routing-ts: devDependencies: @@ -167,7 +212,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) endpoint-agent: devDependencies: @@ -185,7 +230,7 @@ importers: version: 5.7.3 vitest: specifier: ^3.2.6 - version: 3.2.7(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0) event-pipeline-ts: dependencies: @@ -213,7 +258,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) full-stack-microservices: devDependencies: @@ -343,13 +388,13 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) nextjs-app: dependencies: next: specifier: ^16.2.9 - version: 16.2.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 16.2.9(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: specifier: ^19.0.0 version: 19.2.6 @@ -399,7 +444,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) webhook-api-ts: dependencies: @@ -427,10 +472,14 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) packages: + '@alienplatform/ai-gateway@file:../packages/ai-gateway': + resolution: {directory: ../packages/ai-gateway, type: directory} + engines: {bun: '>=1.0.23', node: '>=18'} + '@alienplatform/bindings@file:../packages/bindings': resolution: {directory: ../packages/bindings, type: directory} engines: {bun: '>=1.0.23', node: '>=18'} @@ -485,40 +534,40 @@ packages: resolution: {integrity: sha512-M0vxsZ8X2LFPNWgCS7XekM/8SUPOEnSWaMDXLoXmO/BxW8M3wV84ijXQx0x+8EcR+945ZOOWFD+9rpmGd3RX3A==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.973.18': - resolution: {integrity: sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA==} + '@aws-sdk/core@3.976.0': + resolution: {integrity: sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.16': - resolution: {integrity: sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==} + '@aws-sdk/credential-provider-env@3.972.60': + resolution: {integrity: sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.18': - resolution: {integrity: sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==} + '@aws-sdk/credential-provider-http@3.972.62': + resolution: {integrity: sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.17': - resolution: {integrity: sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==} + '@aws-sdk/credential-provider-ini@3.973.5': + resolution: {integrity: sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.17': - resolution: {integrity: sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==} + '@aws-sdk/credential-provider-login@3.972.67': + resolution: {integrity: sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.18': - resolution: {integrity: sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==} + '@aws-sdk/credential-provider-node@3.972.71': + resolution: {integrity: sha512-HIg7Q2osBzajQwL+1Vkyh2E7Gim3eTNb9RHIsOxDGjW0eZg4oEKtRs5sioCnc73ilhaOm4gX2lHVF8J7+nt2rg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.16': - resolution: {integrity: sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==} + '@aws-sdk/credential-provider-process@3.972.60': + resolution: {integrity: sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.17': - resolution: {integrity: sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==} + '@aws-sdk/credential-provider-sso@3.973.4': + resolution: {integrity: sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.17': - resolution: {integrity: sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==} + '@aws-sdk/credential-provider-web-identity@3.972.66': + resolution: {integrity: sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-host-header@3.972.7': @@ -537,20 +586,24 @@ packages: resolution: {integrity: sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.996.7': - resolution: {integrity: sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==} + '@aws-sdk/nested-clients@3.997.34': + resolution: {integrity: sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA==} engines: {node: '>=20.0.0'} '@aws-sdk/region-config-resolver@3.972.7': resolution: {integrity: sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1004.0': - resolution: {integrity: sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==} + '@aws-sdk/signature-v4-multi-region@3.996.41': + resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.5': - resolution: {integrity: sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==} + '@aws-sdk/token-providers@3.1092.0': + resolution: {integrity: sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} engines: {node: '>=20.0.0'} '@aws-sdk/util-endpoints@3.996.4': @@ -573,14 +626,18 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.972.10': - resolution: {integrity: sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==} + '@aws-sdk/xml-builder@3.972.36': + resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.3': resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} engines: {node: '>=18.0.0'} + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@azure-rest/core-client@2.5.1': resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} engines: {node: '>=20.0.0'} @@ -1164,6 +1221,10 @@ packages: peerDependencies: openapi-types: '*' + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1333,16 +1394,16 @@ packages: resolution: {integrity: sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.9': - resolution: {integrity: sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==} + '@smithy/core@3.29.7': + resolution: {integrity: sha512-BiEE2bnnGoPKdlGe3L+gOYORDHFGPuYVRLP7iUow/Sflm0B4hC4XY3FC1MRuc7ltzpW2xNnXopKi34TTkULlKQ==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.11': - resolution: {integrity: sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==} + '@smithy/credential-provider-imds@4.4.12': + resolution: {integrity: sha512-ZZPDbl/aRp77aycuoMlo3BTayT4CE2a3uoqETYZU5ySnVbhpl5IJiY7dCZedn+ZusyDLqVv44IvKBiXd2/nK0Q==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.13': - resolution: {integrity: sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==} + '@smithy/fetch-http-handler@5.6.9': + resolution: {integrity: sha512-EJktha5m5MXCwzdXrlWyqb9UCNHNFKlg+PmTpRsdX3dncJPTiqYleM9OKj2mLgdVJHR01d2tU4alG+z2NdH5rQ==} engines: {node: '>=18.0.0'} '@smithy/hash-node@4.2.11': @@ -1385,8 +1446,8 @@ packages: resolution: {integrity: sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.4.14': - resolution: {integrity: sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==} + '@smithy/node-http-handler@4.9.9': + resolution: {integrity: sha512-xVBZ3hptB99iNO9XyWqEhC7KD9bP9UPXhuy3h5Y2ItCfBv160D9IIC/Fmmp3EbnWwit4C+KVqlSE+E29Nk/pPg==} engines: {node: '>=18.0.0'} '@smithy/property-provider@4.2.11': @@ -1397,10 +1458,6 @@ packages: resolution: {integrity: sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.2.11': - resolution: {integrity: sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==} - engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.11': resolution: {integrity: sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==} engines: {node: '>=18.0.0'} @@ -1413,16 +1470,16 @@ packages: resolution: {integrity: sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.3.11': - resolution: {integrity: sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==} + '@smithy/signature-v4@5.6.8': + resolution: {integrity: sha512-iGBm6hIwD2MGvVRSgrjVWa4FXtXDq3akxu0DCpnkmBo0xtEHZ/siMRt7ycfZAefYr2UdywUgmGtoRLaq5u56pg==} engines: {node: '>=18.0.0'} '@smithy/smithy-client@4.12.3': resolution: {integrity: sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==} engines: {node: '>=18.0.0'} - '@smithy/types@4.13.0': - resolution: {integrity: sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==} + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} engines: {node: '>=18.0.0'} '@smithy/url-parser@4.2.11': @@ -1481,10 +1538,6 @@ packages: resolution: {integrity: sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==} engines: {node: '>=18.0.0'} - '@smithy/util-uri-escape@4.2.2': - resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} - engines: {node: '>=18.0.0'} - '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} @@ -1662,6 +1715,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1674,6 +1730,9 @@ packages: '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} @@ -1979,13 +2038,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-xml-builder@1.0.0: - resolution: {integrity: sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==} - - fast-xml-parser@5.4.1: - resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} - hasBin: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2582,9 +2634,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@2.2.0: - resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} - stubs@3.0.0: resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} @@ -2839,6 +2888,14 @@ packages: snapshots: + '@alienplatform/ai-gateway@file:../packages/ai-gateway(@types/json-schema@7.0.15)(openapi-types@12.1.3)': + dependencies: + '@alienplatform/core': file:../packages/core(@types/json-schema@7.0.15)(openapi-types@12.1.3) + zod: 4.3.2 + transitivePeerDependencies: + - '@types/json-schema' + - openapi-types + '@alienplatform/bindings@file:../packages/bindings(@types/json-schema@7.0.15)(openapi-types@12.1.3)': dependencies: '@alienplatform/core': file:../packages/core(@types/json-schema@7.0.15)(openapi-types@12.1.3) @@ -2864,6 +2921,7 @@ snapshots: '@alienplatform/sdk@file:../packages/sdk(@types/json-schema@7.0.15)(openapi-types@12.1.3)': dependencies: + '@alienplatform/ai-gateway': file:../packages/ai-gateway(@types/json-schema@7.0.15)(openapi-types@12.1.3) '@alienplatform/bindings': file:../packages/bindings(@types/json-schema@7.0.15)(openapi-types@12.1.3) '@alienplatform/core': file:../packages/core(@types/json-schema@7.0.15)(openapi-types@12.1.3) '@bufbuild/protobuf': 2.11.0 @@ -2900,7 +2958,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -2909,7 +2967,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 tslib: 2.8.1 optional: true @@ -2920,7 +2978,7 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 optional: true @@ -2929,20 +2987,20 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.18 - '@aws-sdk/credential-provider-node': 3.972.18 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/credential-provider-node': 3.972.71 '@aws-sdk/middleware-host-header': 3.972.7 '@aws-sdk/middleware-logger': 3.972.7 '@aws-sdk/middleware-recursion-detection': 3.972.7 '@aws-sdk/middleware-user-agent': 3.972.19 '@aws-sdk/region-config-resolver': 3.972.7 - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 '@aws-sdk/util-endpoints': 3.996.4 '@aws-sdk/util-user-agent-browser': 3.972.7 '@aws-sdk/util-user-agent-node': 3.973.4 '@smithy/config-resolver': 4.4.10 - '@smithy/core': 3.23.9 - '@smithy/fetch-http-handler': 5.3.13 + '@smithy/core': 3.29.7 + '@smithy/fetch-http-handler': 5.6.9 '@smithy/hash-node': 4.2.11 '@smithy/invalid-dependency': 4.2.11 '@smithy/middleware-content-length': 4.2.11 @@ -2951,10 +3009,10 @@ snapshots: '@smithy/middleware-serde': 4.2.12 '@smithy/middleware-stack': 4.2.11 '@smithy/node-config-provider': 4.3.11 - '@smithy/node-http-handler': 4.4.14 + '@smithy/node-http-handler': 4.9.9 '@smithy/protocol-http': 5.3.11 '@smithy/smithy-client': 4.12.3 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 '@smithy/url-parser': 4.2.11 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 @@ -2971,247 +3029,195 @@ snapshots: - aws-crt optional: true - '@aws-sdk/core@3.973.18': + '@aws-sdk/core@3.976.0': dependencies: - '@aws-sdk/types': 3.973.5 - '@aws-sdk/xml-builder': 3.972.10 - '@smithy/core': 3.23.9 - '@smithy/node-config-provider': 4.3.11 - '@smithy/property-provider': 4.2.11 - '@smithy/protocol-http': 5.3.11 - '@smithy/signature-v4': 5.3.11 - '@smithy/smithy-client': 4.12.3 - '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.2 - '@smithy/util-middleware': 4.2.11 - '@smithy/util-utf8': 4.2.2 + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.36 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.29.7 + '@smithy/signature-v4': 5.6.8 + '@smithy/types': 4.16.1 + bowser: 2.14.1 tslib: 2.8.1 optional: true - '@aws-sdk/credential-provider-env@3.972.16': + '@aws-sdk/credential-provider-env@3.972.60': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/types': 3.973.5 - '@smithy/property-provider': 4.2.11 - '@smithy/types': 4.13.0 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@aws-sdk/credential-provider-http@3.972.18': + '@aws-sdk/credential-provider-http@3.972.62': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/types': 3.973.5 - '@smithy/fetch-http-handler': 5.3.13 - '@smithy/node-http-handler': 4.4.14 - '@smithy/property-provider': 4.2.11 - '@smithy/protocol-http': 5.3.11 - '@smithy/smithy-client': 4.12.3 - '@smithy/types': 4.13.0 - '@smithy/util-stream': 4.5.17 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/fetch-http-handler': 5.6.9 + '@smithy/node-http-handler': 4.9.9 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@aws-sdk/credential-provider-ini@3.972.17': - dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/credential-provider-env': 3.972.16 - '@aws-sdk/credential-provider-http': 3.972.18 - '@aws-sdk/credential-provider-login': 3.972.17 - '@aws-sdk/credential-provider-process': 3.972.16 - '@aws-sdk/credential-provider-sso': 3.972.17 - '@aws-sdk/credential-provider-web-identity': 3.972.17 - '@aws-sdk/nested-clients': 3.996.7 - '@aws-sdk/types': 3.973.5 - '@smithy/credential-provider-imds': 4.2.11 - '@smithy/property-provider': 4.2.11 - '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@aws-sdk/credential-provider-ini@3.973.5': + dependencies: + '@aws-sdk/core': 3.976.0 + '@aws-sdk/credential-provider-env': 3.972.60 + '@aws-sdk/credential-provider-http': 3.972.62 + '@aws-sdk/credential-provider-login': 3.972.67 + '@aws-sdk/credential-provider-process': 3.972.60 + '@aws-sdk/credential-provider-sso': 3.973.4 + '@aws-sdk/credential-provider-web-identity': 3.972.66 + '@aws-sdk/nested-clients': 3.997.34 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/credential-provider-imds': 4.4.12 + '@smithy/types': 4.16.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt optional: true - '@aws-sdk/credential-provider-login@3.972.17': + '@aws-sdk/credential-provider-login@3.972.67': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/nested-clients': 3.996.7 - '@aws-sdk/types': 3.973.5 - '@smithy/property-provider': 4.2.11 - '@smithy/protocol-http': 5.3.11 - '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/nested-clients': 3.997.34 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt optional: true - '@aws-sdk/credential-provider-node@3.972.18': + '@aws-sdk/credential-provider-node@3.972.71': dependencies: - '@aws-sdk/credential-provider-env': 3.972.16 - '@aws-sdk/credential-provider-http': 3.972.18 - '@aws-sdk/credential-provider-ini': 3.972.17 - '@aws-sdk/credential-provider-process': 3.972.16 - '@aws-sdk/credential-provider-sso': 3.972.17 - '@aws-sdk/credential-provider-web-identity': 3.972.17 - '@aws-sdk/types': 3.973.5 - '@smithy/credential-provider-imds': 4.2.11 - '@smithy/property-provider': 4.2.11 - '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@aws-sdk/credential-provider-env': 3.972.60 + '@aws-sdk/credential-provider-http': 3.972.62 + '@aws-sdk/credential-provider-ini': 3.973.5 + '@aws-sdk/credential-provider-process': 3.972.60 + '@aws-sdk/credential-provider-sso': 3.973.4 + '@aws-sdk/credential-provider-web-identity': 3.972.66 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/credential-provider-imds': 4.4.12 + '@smithy/types': 4.16.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt optional: true - '@aws-sdk/credential-provider-process@3.972.16': + '@aws-sdk/credential-provider-process@3.972.60': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/types': 3.973.5 - '@smithy/property-provider': 4.2.11 - '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@aws-sdk/credential-provider-sso@3.972.17': + '@aws-sdk/credential-provider-sso@3.973.4': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/nested-clients': 3.996.7 - '@aws-sdk/token-providers': 3.1004.0 - '@aws-sdk/types': 3.973.5 - '@smithy/property-provider': 4.2.11 - '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/nested-clients': 3.997.34 + '@aws-sdk/token-providers': 3.1092.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt optional: true - '@aws-sdk/credential-provider-web-identity@3.972.17': + '@aws-sdk/credential-provider-web-identity@3.972.66': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/nested-clients': 3.996.7 - '@aws-sdk/types': 3.973.5 - '@smithy/property-provider': 4.2.11 - '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/nested-clients': 3.997.34 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt optional: true '@aws-sdk/middleware-host-header@3.972.7': dependencies: - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@aws-sdk/middleware-logger@3.972.7': dependencies: - '@aws-sdk/types': 3.973.5 - '@smithy/types': 4.13.0 + '@aws-sdk/types': 3.974.2 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@aws-sdk/middleware-recursion-detection@3.972.7': dependencies: - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 '@aws/lambda-invoke-store': 0.2.3 '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@aws-sdk/middleware-user-agent@3.972.19': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/types': 3.973.5 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/types': 3.974.2 '@aws-sdk/util-endpoints': 3.996.4 - '@smithy/core': 3.23.9 + '@smithy/core': 3.29.7 '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 '@smithy/util-retry': 4.2.11 tslib: 2.8.1 optional: true - '@aws-sdk/nested-clients@3.996.7': + '@aws-sdk/nested-clients@3.997.34': dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.18 - '@aws-sdk/middleware-host-header': 3.972.7 - '@aws-sdk/middleware-logger': 3.972.7 - '@aws-sdk/middleware-recursion-detection': 3.972.7 - '@aws-sdk/middleware-user-agent': 3.972.19 - '@aws-sdk/region-config-resolver': 3.972.7 - '@aws-sdk/types': 3.973.5 - '@aws-sdk/util-endpoints': 3.996.4 - '@aws-sdk/util-user-agent-browser': 3.972.7 - '@aws-sdk/util-user-agent-node': 3.973.4 - '@smithy/config-resolver': 4.4.10 - '@smithy/core': 3.23.9 - '@smithy/fetch-http-handler': 5.3.13 - '@smithy/hash-node': 4.2.11 - '@smithy/invalid-dependency': 4.2.11 - '@smithy/middleware-content-length': 4.2.11 - '@smithy/middleware-endpoint': 4.4.23 - '@smithy/middleware-retry': 4.4.40 - '@smithy/middleware-serde': 4.2.12 - '@smithy/middleware-stack': 4.2.11 - '@smithy/node-config-provider': 4.3.11 - '@smithy/node-http-handler': 4.4.14 - '@smithy/protocol-http': 5.3.11 - '@smithy/smithy-client': 4.12.3 - '@smithy/types': 4.13.0 - '@smithy/url-parser': 4.2.11 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.39 - '@smithy/util-defaults-mode-node': 4.2.42 - '@smithy/util-endpoints': 3.3.2 - '@smithy/util-middleware': 4.2.11 - '@smithy/util-retry': 4.2.11 - '@smithy/util-utf8': 4.2.2 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/fetch-http-handler': 5.6.9 + '@smithy/node-http-handler': 4.9.9 + '@smithy/types': 4.16.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt optional: true '@aws-sdk/region-config-resolver@3.972.7': dependencies: - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 '@smithy/config-resolver': 4.4.10 '@smithy/node-config-provider': 4.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@aws-sdk/token-providers@3.1004.0': + '@aws-sdk/signature-v4-multi-region@3.996.41': dependencies: - '@aws-sdk/core': 3.973.18 - '@aws-sdk/nested-clients': 3.996.7 - '@aws-sdk/types': 3.973.5 - '@smithy/property-provider': 4.2.11 - '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.8 + '@smithy/types': 4.16.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt optional: true - '@aws-sdk/types@3.973.5': + '@aws-sdk/token-providers@3.1092.0': dependencies: - '@smithy/types': 4.13.0 + '@aws-sdk/core': 3.976.0 + '@aws-sdk/nested-clients': 3.997.34 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + optional: true + + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@aws-sdk/util-endpoints@3.996.4': dependencies: - '@aws-sdk/types': 3.973.5 - '@smithy/types': 4.13.0 + '@aws-sdk/types': 3.974.2 + '@smithy/types': 4.16.1 '@smithy/url-parser': 4.2.11 '@smithy/util-endpoints': 3.3.2 tslib: 2.8.1 @@ -3224,8 +3230,8 @@ snapshots: '@aws-sdk/util-user-agent-browser@3.972.7': dependencies: - '@aws-sdk/types': 3.973.5 - '@smithy/types': 4.13.0 + '@aws-sdk/types': 3.974.2 + '@smithy/types': 4.16.1 bowser: 2.14.1 tslib: 2.8.1 optional: true @@ -3233,22 +3239,24 @@ snapshots: '@aws-sdk/util-user-agent-node@3.973.4': dependencies: '@aws-sdk/middleware-user-agent': 3.972.19 - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.974.2 '@smithy/node-config-provider': 4.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@aws-sdk/xml-builder@3.972.10': + '@aws-sdk/xml-builder@3.972.36': dependencies: - '@smithy/types': 4.13.0 - fast-xml-parser: 5.4.1 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@aws/lambda-invoke-store@0.2.3': optional: true + '@aws/lambda-invoke-store@0.3.0': + optional: true + '@azure-rest/core-client@2.5.1': dependencies: '@azure/abort-controller': 2.1.2 @@ -3823,6 +3831,9 @@ snapshots: transitivePeerDependencies: - '@types/json-schema' + '@opentelemetry/api@1.9.0': + optional: true + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -3927,55 +3938,43 @@ snapshots: '@smithy/abort-controller@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/config-resolver@4.4.10': dependencies: '@smithy/node-config-provider': 4.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 '@smithy/util-config-provider': 4.2.2 '@smithy/util-endpoints': 3.3.2 '@smithy/util-middleware': 4.2.11 tslib: 2.8.1 optional: true - '@smithy/core@3.23.9': + '@smithy/core@3.29.7': dependencies: - '@smithy/middleware-serde': 4.2.12 - '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-middleware': 4.2.11 - '@smithy/util-stream': 4.5.17 - '@smithy/util-utf8': 4.2.2 - '@smithy/uuid': 1.1.2 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@smithy/credential-provider-imds@4.2.11': + '@smithy/credential-provider-imds@4.4.12': dependencies: - '@smithy/node-config-provider': 4.3.11 - '@smithy/property-provider': 4.2.11 - '@smithy/types': 4.13.0 - '@smithy/url-parser': 4.2.11 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@smithy/fetch-http-handler@5.3.13': + '@smithy/fetch-http-handler@5.6.9': dependencies: - '@smithy/protocol-http': 5.3.11 - '@smithy/querystring-builder': 4.2.11 - '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/hash-node@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 '@smithy/util-buffer-from': 4.2.2 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 @@ -3983,7 +3982,7 @@ snapshots: '@smithy/invalid-dependency@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true @@ -4000,17 +3999,17 @@ snapshots: '@smithy/middleware-content-length@4.2.11': dependencies: '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/middleware-endpoint@4.4.23': dependencies: - '@smithy/core': 3.23.9 + '@smithy/core': 3.29.7 '@smithy/middleware-serde': 4.2.12 '@smithy/node-config-provider': 4.3.11 '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 '@smithy/url-parser': 4.2.11 '@smithy/util-middleware': 4.2.11 tslib: 2.8.1 @@ -4022,7 +4021,7 @@ snapshots: '@smithy/protocol-http': 5.3.11 '@smithy/service-error-classification': 4.2.11 '@smithy/smithy-client': 4.12.3 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 '@smithy/util-middleware': 4.2.11 '@smithy/util-retry': 4.2.11 '@smithy/uuid': 1.1.2 @@ -4032,13 +4031,13 @@ snapshots: '@smithy/middleware-serde@4.2.12': dependencies: '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/middleware-stack@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true @@ -4046,79 +4045,65 @@ snapshots: dependencies: '@smithy/property-provider': 4.2.11 '@smithy/shared-ini-file-loader': 4.4.6 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@smithy/node-http-handler@4.4.14': + '@smithy/node-http-handler@4.9.9': dependencies: - '@smithy/abort-controller': 4.2.11 - '@smithy/protocol-http': 5.3.11 - '@smithy/querystring-builder': 4.2.11 - '@smithy/types': 4.13.0 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/property-provider@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/protocol-http@5.3.11': dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - optional: true - - '@smithy/querystring-builder@4.2.11': - dependencies: - '@smithy/types': 4.13.0 - '@smithy/util-uri-escape': 4.2.2 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/querystring-parser@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/service-error-classification@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 optional: true '@smithy/shared-ini-file-loader@4.4.6': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true - '@smithy/signature-v4@5.3.11': + '@smithy/signature-v4@5.6.8': dependencies: - '@smithy/is-array-buffer': 4.2.2 - '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-middleware': 4.2.11 - '@smithy/util-uri-escape': 4.2.2 - '@smithy/util-utf8': 4.2.2 + '@smithy/core': 3.29.7 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/smithy-client@4.12.3': dependencies: - '@smithy/core': 3.23.9 + '@smithy/core': 3.29.7 '@smithy/middleware-endpoint': 4.4.23 '@smithy/middleware-stack': 4.2.11 '@smithy/protocol-http': 5.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 '@smithy/util-stream': 4.5.17 tslib: 2.8.1 optional: true - '@smithy/types@4.13.0': + '@smithy/types@4.16.1': dependencies: tslib: 2.8.1 optional: true @@ -4126,7 +4111,7 @@ snapshots: '@smithy/url-parser@4.2.11': dependencies: '@smithy/querystring-parser': 4.2.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true @@ -4168,25 +4153,25 @@ snapshots: dependencies: '@smithy/property-provider': 4.2.11 '@smithy/smithy-client': 4.12.3 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/util-defaults-mode-node@4.2.42': dependencies: '@smithy/config-resolver': 4.4.10 - '@smithy/credential-provider-imds': 4.2.11 + '@smithy/credential-provider-imds': 4.4.12 '@smithy/node-config-provider': 4.3.11 '@smithy/property-provider': 4.2.11 '@smithy/smithy-client': 4.12.3 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/util-endpoints@3.3.2': dependencies: '@smithy/node-config-provider': 4.3.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true @@ -4197,22 +4182,22 @@ snapshots: '@smithy/util-middleware@4.2.11': dependencies: - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/util-retry@4.2.11': dependencies: '@smithy/service-error-classification': 4.2.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true '@smithy/util-stream@4.5.17': dependencies: - '@smithy/fetch-http-handler': 5.3.13 - '@smithy/node-http-handler': 4.4.14 - '@smithy/types': 4.13.0 + '@smithy/fetch-http-handler': 5.6.9 + '@smithy/node-http-handler': 4.9.9 + '@smithy/types': 4.16.1 '@smithy/util-base64': 4.3.2 '@smithy/util-buffer-from': 4.2.2 '@smithy/util-hex-encoding': 4.2.2 @@ -4220,11 +4205,6 @@ snapshots: tslib: 2.8.1 optional: true - '@smithy/util-uri-escape@4.2.2': - dependencies: - tslib: 2.8.1 - optional: true - '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 @@ -4240,7 +4220,7 @@ snapshots: '@smithy/util-waiter@4.2.11': dependencies: '@smithy/abort-controller': 4.2.11 - '@smithy/types': 4.13.0 + '@smithy/types': 4.16.1 tslib: 2.8.1 optional: true @@ -4489,6 +4469,11 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + optional: true + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -4498,6 +4483,9 @@ snapshots: '@types/long@4.0.2': optional: true + '@types/ms@2.1.0': + optional: true + '@types/node@22.19.17': dependencies: undici-types: 6.21.0 @@ -4859,15 +4847,6 @@ snapshots: extend@3.0.2: optional: true - fast-xml-builder@1.0.0: - optional: true - - fast-xml-parser@5.4.1: - dependencies: - fast-xml-builder: 1.0.0 - strnum: 2.2.0 - optional: true - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -5226,7 +5205,7 @@ snapshots: nanoid@3.3.11: {} - next@16.2.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next@16.2.9(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@next/env': 16.2.9 '@swc/helpers': 0.5.15 @@ -5245,6 +5224,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.9 '@next/swc-win32-arm64-msvc': 16.2.9 '@next/swc-win32-x64-msvc': 16.2.9 + '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -5549,9 +5529,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.2.0: - optional: true - stubs@3.0.0: optional: true @@ -5705,7 +5682,7 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0) - vitest@3.2.7(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0): + vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 @@ -5731,6 +5708,7 @@ snapshots: vite-node: 3.2.4(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.13 '@types/node': 22.19.17 transitivePeerDependencies: - jiti @@ -5746,7 +5724,7 @@ snapshots: - tsx - yaml - vitest@3.2.7(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0): + vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 @@ -5772,6 +5750,7 @@ snapshots: vite-node: 3.2.4(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.13 '@types/node': 24.12.0 transitivePeerDependencies: - jiti diff --git a/examples/pnpm-workspace.yaml b/examples/pnpm-workspace.yaml index ed849fbfc..45eacc56c 100644 --- a/examples/pnpm-workspace.yaml +++ b/examples/pnpm-workspace.yaml @@ -1,4 +1,6 @@ packages: + - ai-quickstart-ts + - ai-finetune-inference-ts - basic-worker-ts - basic-worker-rs - remote-worker-ts diff --git a/internal-docs/alien/ai-finetune-runtime-design.md b/internal-docs/alien/ai-finetune-runtime-design.md new file mode 100644 index 000000000..7e6c71736 --- /dev/null +++ b/internal-docs/alien/ai-finetune-runtime-design.md @@ -0,0 +1,105 @@ +# AI fine-tuning: runtime, API-triggered design + +Status: implementing. Supersedes the deploy-time tuning model. + +## Why this changed + +The first implementation submitted the cloud tuning job at **deploy time**, inside the +`Ai` resource controller, and blocked the resource in a `WaitingForTuningJob` state until +the job completed. Three problems made that wrong: + +1. **Empty-bucket ordering.** The job wants training data at deploy time, but apps upload + data at runtime (there is no data in the bucket on first deploy). +2. **Hours-long deploys.** Fine-tuning takes minutes to hours; a resource that isn't + `Ready` until training finishes makes `alien release` block for hours. +3. **Retraining = redeploy.** Training on new data meant editing the resource and + redeploying. + +Fine-tuning is fundamentally an **imperative, long-running job**, not a declarative +resource state. So we move the trigger to runtime. + +## The model + +- `.finetune({...})` on the `Ai` resource is now a **capability declaration**: it says + "this gateway may fine-tune `baseModel`, reading data from `trainingData`, serving the + result as `servedModelId`", and it drives the `ai/finetune` permission grant. It does + **not** start a job. +- The `Ai` controller reaches `Ready` immediately (no tuning states). +- At runtime the app calls the gateway to start/track jobs: + - `ai("llm").finetune({ trainingKey? }) -> { jobId }` — submits the cloud tuning job. + - `ai("llm").finetuneStatus(jobId) -> { status, model? }` — polls it. +- Inference against `servedModelId` works as soon as the tuned model is `Active`, via + **rediscovery by convention** (below) — no state store. + +## Gateway control-plane surface + +New routes on the in-process gateway (alongside the inference proxy): + +- `POST //v1/finetune` — body `{ trainingKey?, baseModel?, method? }`. Submits + the provider job using the binding's ambient credential and the `finetune` capability + from the binding. Returns `{ jobId, servedModel }`. +- `GET //v1/finetune/` — returns `{ status: "pending"|"running"|"succeeded"|"failed", model? }`. + +The gateway already holds the ambient credential and signs arbitrary +host/service requests (`AmbientCred::authorize(req, service)`), so control-plane calls +(`bedrock` control host, Vertex `tuningJobs`, Foundry `fine_tuning.jobs`) reuse the same +credential path as inference. No new credential wiring. + +### Per-cloud provider trait + +```rust +#[async_trait] +trait FineTuneProvider { + async fn submit(&self, spec: &FineTuneRequest) -> Result; + async fn status(&self, job: &str) -> Result; // by job id + async fn resolve_served_model(&self, served_id: &str) -> Result>; // rediscovery +} +``` + +- **Bedrock**: submit = `CreateModelCustomizationJob`; status = `GetModelCustomizationJob`; + rediscovery = `GetCustomModel()` → if `modelStatus == Active`, its + `modelArn` is the upstream id. `GetCustomModel` accepts the model **name**, so no ARN + needs storing. +- **Vertex**: submit = `POST tuningJobs`; status = `GET tuningJobs/{id}`; rediscovery = + the tuned endpoint id derived from the job / a list filtered by display name. +- **Foundry**: submit = `POST fine_tuning/jobs`; status = `GET fine_tuning/jobs/{id}`; + on success the job carries `fine_tuned_model`; rediscovery = the deployment named after + `servedModelId` (create-on-first-success), or `GET deployments/{name}`. + +## Rediscovery by convention (stateless) + +The gateway is per-process and stateless. A job started by one worker completes on the +cloud's side hours later, possibly after that worker is gone. So the gateway does **not** +track job state across restarts. Instead: + +- The tuned model's cloud name is **deterministic** from the binding + `servedModelId`. +- On an inference request for `servedModelId`, if the route has no cached tuned upstream, + the gateway calls `resolve_served_model(servedId)`; if the provider reports the model + `Active`, it caches and routes to it. If not ready, it returns `model not available`. +- `GET /finetune/` likewise queries the cloud live each call. + +No storage dependency, no background poller. The trade-off: a completed job is only +"noticed" on the next status poll or inference request (fine — it's a pull model). + +## Role / credentials note (fixes the deploy-time role-ARN bug) + +The deploy-time version derived a job `roleArn` (`{prefix}-{id}`) that didn't reliably +exist. In the runtime model the gateway submits the job under the **workload's ambient +identity** — the same identity it uses for inference. Bedrock's `CreateModelCustomizationJob` +still needs a `roleArn` it can assume to read S3 / write output, so the gateway resolves +the workload's actual execution role (or a dedicated finetune role the `ai/finetune` +permission set provisions with a `bedrock.amazonaws.com` trust policy). This is resolved +at submit time from the binding, not guessed from a naming convention. + +## Layers touched + +- `alien-gateway`: new `finetune` module (trait + 3 providers), 2 routes, tuned-model + cache + rediscovery in the router. +- `packages/ai-gateway` (SDK): `finetune()` / `finetuneStatus()` on the `ai()` client. +- `alien-core`: binding carries the `finetune` capability (baseModel, trainingData bucket, + servedModelId, method) so the gateway can submit without a control-plane round-trip to + the resource. `FinetuneSpec` stays on the resource as the declaration. +- `alien-infra` controllers: drop `SubmittingTuningJob`/`WaitingForTuningJob`; reach + `Ready` immediately; still emit the `ai/finetune` grant and the capability in the binding. +- Example `ai-finetune-inference-ts`: `POST /dataset` → `POST /finetune` → poll + `/finetune/status` → `POST /chat-tuned`. diff --git a/packages/ai-gateway/PACKAGE_LAYOUT.md b/packages/ai-gateway/PACKAGE_LAYOUT.md new file mode 100644 index 000000000..f972a7f1b --- /dev/null +++ b/packages/ai-gateway/PACKAGE_LAYOUT.md @@ -0,0 +1,77 @@ +# `@alienplatform/ai-gateway` — package layout contract + +> Contract document. The names, subpaths, and dependency rules below are binding. +> Implementers may not rename anything pinned here. + +## Purpose + +`@alienplatform/ai-gateway` is the public TypeScript wrapper for the Alien AI +gateway: a thin loader over a napi-rs native addon. The Rust crate `alien-gateway` +is the single gateway implementation (loopback HTTP proxy, per-cloud ambient +credential injection, curated-catalog model rewrite, protocol selection, streaming +pass-through). The TypeScript layer only loads the addon and starts it once per +process, returning the loopback base URL; every request and SSE stream then flows +over the loopback HTTP socket into the Rust gateway. No gateway logic lives in JS, +and nothing crosses the napi boundary per request. + +## Public surface — all exports from `"."` + +| Export | Kind | Signature sketch | Notes | +|---|---|---|---| +| `startAiGateway` | function | `startAiGateway(): Promise<{ url: string }>` | Starts the in-process gateway once (idempotent) and returns its running handle. Hold it for the process lifetime. | +| `ai` | function | `ai(name: string): Ai` | An OpenAI-compatible client for the named binding (ambient or BYO-key `External`). | +| `getAiConnection` | function | `getAiConnection(name: string): Promise` | Starts the gateway on first use; returns `{ baseURL, apiKey? }` for any OpenAI-compatible client. | +| `Ai` | class | `chat.completions.create()`, `responses.create()`, `getAvailableModels()` | What `ai(name)` returns. | +| `AiConnection` | type | `{ baseURL: string; apiKey?: string }` | Result of `getAiConnection`. | +| `AiModel`, `ChatCompletionCreateParams`, `ResponseCreateParams` | types | — | Request/response shapes for the `Ai` client. | +| `aiBindingEnvVarName`, `isExternalAiBinding`, `parseAiBinding` | functions | — | Binding-env helpers. | +| `AiBinding`, `AmbientAiBinding`, `ExternalAiBinding` | types | — | Parsed binding shapes. | +| `AiTransportError`, `AiUpstreamError`, `BindingNotFoundError`, `InvalidBindingConfigError` | error classes | — | Typed errors thrown by the `Ai` client. | + +### Intentionally not exposed + +- No per-cloud credential surface — the Rust gateway injects the ambient credential. +- No gateway logic in JS — the addon loads and starts the gateway; every request and SSE + stream flows over the loopback HTTP socket into the Rust gateway. + +## Exports map + +Two entry points; every condition carries `types`. + +```jsonc +{ + ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./native": { "types": "./dist/native.d.ts", "import": "./dist/native.js" } +} +``` + +- `.` — the lazy-loading entry (resolves the per-platform addon on first use). +- `./native` — the static-embed entry for `bun build --compile` (imports the addon + through the literal `./alien-ai-gateway.node`, staged next to `dist/native.js`). + +## Native addon + prebuilds + +- Rust addon crate: `crates/alien-ai-gateway-node` (napi `binaryName` + `alien-ai-gateway-node`, `packageName` `@alienplatform/ai-gateway`). +- The addon loader (`src/loader.ts`) resolves, in order: `ALIEN_AI_GATEWAY_ADDON_PATH` + → the per-platform prebuild `@alienplatform/ai-gateway-` (from + `optionalDependencies`, injected at publish time) → the version-gated locally-built + addon under `crates/alien-ai-gateway-node`. +- `TRIPLES` (the prebuild set) must mirror `napi.targets` in the crate's + `package.json`: `darwin-arm64`, `darwin-x64`, `linux-x64-gnu`, `linux-arm64-gnu`. + Only glibc Linux prebuilds are published; musl throws a clear unsupported-platform + error. + +## Dependency boundaries + +- Runtime dependency: `@alienplatform/core` only. +- No JS cloud SDKs, no gRPC, no `@alienplatform/sdk`. +- The source manifest carries no `optionalDependencies`; the release pipeline injects + them at publish time from `scripts/inject-optional-deps.mjs`. + +## Behavior contract + +- Importing the package performs no I/O (`sideEffects: false`); the addon loads on the + first `startAiGateway()` / `getAiConnection()` call. +- The gateway starts once per process and is shared across binding names (routed by + `/`); the handle is held for the process lifetime. diff --git a/packages/ai-gateway/npm/.gitignore b/packages/ai-gateway/npm/.gitignore new file mode 100644 index 000000000..96c63d17f --- /dev/null +++ b/packages/ai-gateway/npm/.gitignore @@ -0,0 +1,4 @@ +# Per-platform prebuild skeletons: only the manifest is committed. The .node addon +# is staged into each dir at build/publish time (locally by `napi build`, in CI by +# the build-addon/publish-ai-gateway jobs) and must never be committed. +*/*.node diff --git a/packages/ai-gateway/npm/darwin-arm64/package.json b/packages/ai-gateway/npm/darwin-arm64/package.json new file mode 100644 index 000000000..6cc064910 --- /dev/null +++ b/packages/ai-gateway/npm/darwin-arm64/package.json @@ -0,0 +1,9 @@ +{ + "name": "@alienplatform/ai-gateway-darwin-arm64", + "version": "0.0.0", + "cpu": ["arm64"], + "main": "alien-ai-gateway-node.darwin-arm64.node", + "files": ["alien-ai-gateway-node.darwin-arm64.node"], + "description": "napi-rs addon exposing the alien AI gateway to JavaScript", + "os": ["darwin"] +} diff --git a/packages/ai-gateway/npm/darwin-x64/package.json b/packages/ai-gateway/npm/darwin-x64/package.json new file mode 100644 index 000000000..977477aaa --- /dev/null +++ b/packages/ai-gateway/npm/darwin-x64/package.json @@ -0,0 +1,9 @@ +{ + "name": "@alienplatform/ai-gateway-darwin-x64", + "version": "0.0.0", + "cpu": ["x64"], + "main": "alien-ai-gateway-node.darwin-x64.node", + "files": ["alien-ai-gateway-node.darwin-x64.node"], + "description": "napi-rs addon exposing the alien AI gateway to JavaScript", + "os": ["darwin"] +} diff --git a/packages/ai-gateway/npm/linux-arm64-gnu/package.json b/packages/ai-gateway/npm/linux-arm64-gnu/package.json new file mode 100644 index 000000000..c12e96d63 --- /dev/null +++ b/packages/ai-gateway/npm/linux-arm64-gnu/package.json @@ -0,0 +1,9 @@ +{ + "name": "@alienplatform/ai-gateway-linux-arm64-gnu", + "version": "0.0.0", + "cpu": ["arm64"], + "main": "alien-ai-gateway-node.linux-arm64-gnu.node", + "files": ["alien-ai-gateway-node.linux-arm64-gnu.node"], + "description": "napi-rs addon exposing the alien AI gateway to JavaScript", + "os": ["linux"] +} diff --git a/packages/ai-gateway/npm/linux-x64-gnu/package.json b/packages/ai-gateway/npm/linux-x64-gnu/package.json new file mode 100644 index 000000000..ccac2f7da --- /dev/null +++ b/packages/ai-gateway/npm/linux-x64-gnu/package.json @@ -0,0 +1,9 @@ +{ + "name": "@alienplatform/ai-gateway-linux-x64-gnu", + "version": "0.0.0", + "cpu": ["x64"], + "main": "alien-ai-gateway-node.linux-x64-gnu.node", + "files": ["alien-ai-gateway-node.linux-x64-gnu.node"], + "description": "napi-rs addon exposing the alien AI gateway to JavaScript", + "os": ["linux"] +} diff --git a/packages/ai-gateway/package.json b/packages/ai-gateway/package.json new file mode 100644 index 000000000..49f37baf1 --- /dev/null +++ b/packages/ai-gateway/package.json @@ -0,0 +1,57 @@ +{ + "name": "@alienplatform/ai-gateway", + "version": "2.1.6", + "type": "module", + "sideEffects": false, + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./native": { + "types": "./dist/native.d.ts", + "import": "./dist/native.js" + } + }, + "files": ["dist"], + "engines": { + "node": ">=18", + "bun": ">=1.0.23" + }, + "scripts": { + "build": "tsdown && tsc -p tsconfig.build.json --emitDeclarationOnly", + "build:addon": "napi build --platform --release --cwd ../../crates/alien-ai-gateway-node", + "prepare": "npm run build", + "test": "vitest run", + "test:ts": "tsc --noEmit", + "format-and-lint": "biome check .", + "format-and-lint:fix": "biome check . --write" + }, + "keywords": ["alien", "ai", "gateway", "napi"], + "author": "Alien Software, Inc. ", + "license": "FSL-1.1-Apache-2.0", + "description": "Thin TypeScript wrapper that starts the Alien AI gateway (a Rust napi-rs addon) in-process", + "homepage": "https://alien.dev", + "repository": { + "type": "git", + "url": "https://github.com/alienplatform/alien.git", + "directory": "packages/ai-gateway" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@alienplatform/typescript-config": "workspace:^", + "@biomejs/biome": "^1.9.4", + "@napi-rs/cli": "^3.0.0", + "@types/node": "^24.0.15", + "tsdown": "^0.13.0", + "typescript": "^5.8.3", + "vitest": "^3.1.4" + }, + "dependencies": { + "@alienplatform/core": "workspace:^", + "zod": "4.3.2" + } +} diff --git a/packages/ai-gateway/scripts/inject-optional-deps.mjs b/packages/ai-gateway/scripts/inject-optional-deps.mjs new file mode 100644 index 000000000..b52c38310 --- /dev/null +++ b/packages/ai-gateway/scripts/inject-optional-deps.mjs @@ -0,0 +1,44 @@ +/** + * Pin the per-platform prebuild packages as exact-version `optionalDependencies` + * of the `@alienplatform/ai-gateway` wrapper, so a published wrapper can only + * resolve the matching-version platform addon. + * + * The release pipeline runs this after rewriting the wrapper's version and before + * publishing. Preferred over `napi prepublish`, which regenerates more than the pin. + */ + +import { readFileSync, readdirSync, writeFileSync } from "node:fs" +import { fileURLToPath } from "node:url" + +const TRIPLES = ["darwin-arm64", "darwin-x64", "linux-x64-gnu", "linux-arm64-gnu"] + +const wrapperManifest = fileURLToPath(new URL("../package.json", import.meta.url)) +const pkg = JSON.parse(readFileSync(wrapperManifest, "utf8")) +const { version } = pkg + +// An unusable version silently JSON.stringify's away (undefined values are dropped), which +// would publish the wrapper with *no* optionalDependencies — every consumer install then +// resolves no addon at all. Fail the release instead. +if (typeof version !== "string" || version === "" || version === "0.0.0") { + throw new Error(`refusing to inject optionalDependencies: package version is '${version}'`) +} + +// TRIPLES must mirror the per-platform packages on disk (PACKAGE_LAYOUT.md pins them). +const onDisk = readdirSync(fileURLToPath(new URL("../npm", import.meta.url)), { + withFileTypes: true, +}) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .sort() +if (onDisk.join() !== [...TRIPLES].sort().join()) { + throw new Error(`npm/ holds [${onDisk}], but TRIPLES is [${[...TRIPLES].sort()}]`) +} + +pkg.optionalDependencies = Object.fromEntries( + TRIPLES.map(triple => [`@alienplatform/ai-gateway-${triple}`, version]), +) + +writeFileSync(wrapperManifest, `${JSON.stringify(pkg, null, 2)}\n`) + +console.log(`Injected optionalDependencies (pinned to ${version}):`) +console.log(JSON.stringify(pkg.optionalDependencies, null, 2)) diff --git a/packages/ai-gateway/scripts/validate-release-version.mjs b/packages/ai-gateway/scripts/validate-release-version.mjs new file mode 100644 index 000000000..5ea0dc7da --- /dev/null +++ b/packages/ai-gateway/scripts/validate-release-version.mjs @@ -0,0 +1,38 @@ +import { readFileSync } from "node:fs" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +const expected = process.argv[2] +if (!expected) { + throw new Error("usage: validate-release-version.mjs ") +} + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..") +const manifests = [ + "packages/ai-gateway/package.json", + "crates/alien-ai-gateway-node/package.json", + "packages/ai-gateway/npm/darwin-arm64/package.json", + "packages/ai-gateway/npm/darwin-x64/package.json", + "packages/ai-gateway/npm/linux-x64-gnu/package.json", + "packages/ai-gateway/npm/linux-arm64-gnu/package.json", +] + +for (const path of manifests) { + const actual = JSON.parse(readFileSync(join(repoRoot, path), "utf8")).version + if (actual !== expected) { + throw new Error(`${path}: expected version ${expected}, got ${actual}`) + } +} + +const cargo = readFileSync(join(repoRoot, "Cargo.toml"), "utf8") +const workspaceVersion = cargo.match(/\[workspace\.package\]\nversion = "([^"]+)"/)?.[1] +if (workspaceVersion !== expected) { + throw new Error(`Cargo workspace: expected version ${expected}, got ${workspaceVersion}`) +} + +const addonCargo = readFileSync(join(repoRoot, "crates/alien-ai-gateway-node/Cargo.toml"), "utf8") +if (!/^version\.workspace = true$/m.test(addonCargo)) { + throw new Error("alien-ai-gateway-node must inherit the validated Cargo workspace version") +} + +console.log(`AI-gateway release manifests are locked to ${expected}`) diff --git a/packages/ai-gateway/src/__tests__/client.test.ts b/packages/ai-gateway/src/__tests__/client.test.ts new file mode 100644 index 000000000..f3c9d2096 --- /dev/null +++ b/packages/ai-gateway/src/__tests__/client.test.ts @@ -0,0 +1,369 @@ +import { AlienError } from "@alienplatform/core" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import { createAiClient } from "../client.js" +import type { Gateway } from "../gateway.js" +import type { RawAiGatewayHandle } from "../loader.js" + +// ───────────────────────────────────────────────────────────────────────────── +// Harness. The `Ai` client is exercised against a BYO-key (External) binding, where +// it talks to the provider directly and the HTTP behavior is mockable via global +// fetch. The ambient path is resolved through a stubbed Gateway (the real Rust +// addon is covered by the crate's own tests); here we only assert the client + +// binding resolution, including the URL seam (`//v1/...`). +// ───────────────────────────────────────────────────────────────────────────── + +const EXTERNAL = JSON.stringify({ service: "external", provider: "openai", apiKey: "sk-test" }) + +const GATEWAY_URL = "http://127.0.0.1:41999" + +const stubGateway: Gateway = { + startAiGateway: () => Promise.resolve({ url: GATEWAY_URL } as unknown as RawAiGatewayHandle), +} + +const { ai, getAiConnection } = createAiClient(stubGateway) + +beforeEach(() => { + vi.stubEnv("ALIEN_LLM_BINDING", EXTERNAL) +}) + +afterEach(() => { + vi.unstubAllEnvs() + vi.unstubAllGlobals() +}) + +function stubFetch(responseBody: unknown, status = 200): ReturnType { + const fetchMock = vi.fn().mockResolvedValue({ + ok: status >= 200 && status < 300, + status, + statusText: status === 200 ? "OK" : "Error", + json: vi.fn().mockResolvedValue(responseBody), + text: vi.fn().mockResolvedValue(JSON.stringify(responseBody)), + }) + vi.stubGlobal("fetch", fetchMock) + return fetchMock +} + +function buildSseBody(chunks: unknown[]): ReadableStream { + const encoder = new TextEncoder() + const lines = [...chunks.map(c => `data: ${JSON.stringify(c)}\n\n`), "data: [DONE]\n\n"].join("") + return new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(lines)) + controller.close() + }, + }) +} + +function stubFetchSse(chunks: unknown[]): ReturnType { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: "OK", + body: buildSseBody(chunks), + json: vi.fn().mockRejectedValue(new Error("not a JSON response")), + text: vi.fn().mockRejectedValue(new Error("not a text response")), + }) + vi.stubGlobal("fetch", fetchMock) + return fetchMock +} + +const callUrl = (m: ReturnType): string => m.mock.calls[0]![0] as string +const callInit = (m: ReturnType): RequestInit => m.mock.calls[0]![1] as RequestInit +const callBody = (m: ReturnType): Record => + JSON.parse(callInit(m).body as string) as Record + +// ───────────────────────────────────────────────────────────────────────────── +// getAiConnection +// ───────────────────────────────────────────────────────────────────────────── + +describe("getAiConnection", () => { + it("resolves an External (BYO-key) binding to the provider directly, with the key", async () => { + expect(await getAiConnection("llm")).toEqual({ + baseURL: "https://api.openai.com/v1", + apiKey: "sk-test", + }) + }) + + it("resolves an ambient-cloud binding to the in-process gateway, with no key", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", JSON.stringify({ service: "bedrock", region: "us-east-2" })) + const conn = await getAiConnection("llm") + // The gateway serves `//v1/...`; the connection carries exactly one `/v1`. + expect(conn.baseURL).toBe(`${GATEWAY_URL}/llm/v1`) + expect(conn.apiKey).toBeUndefined() + }) + + it("uses the resource id as the gateway route segment", async () => { + vi.stubEnv("ALIEN_MY_LLM_BINDING", JSON.stringify({ service: "bedrock", region: "us-east-2" })) + const conn = await getAiConnection("My-LLM") + expect(conn.baseURL).toBe(`${GATEWAY_URL}/my-llm/v1`) + }) + + it("honors ALIEN_AI_LOCAL_BASE_URL to point at any OpenAI-compatible provider", async () => { + vi.stubEnv("ALIEN_AI_LOCAL_BASE_URL", "http://localhost:11434") + expect((await getAiConnection("llm")).baseURL).toBe("http://localhost:11434/v1") + }) + + it("throws BINDING_NOT_FOUND when the binding env var is missing", async () => { + await expect(getAiConnection("unlinked")).rejects.toMatchObject({ + code: "BINDING_NOT_FOUND", + httpStatusCode: 404, + }) + }) + + it("routes a service tag this SDK predates to the gateway (no client-side rejection)", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", JSON.stringify({ service: "sagemaker", endpoint: "x" })) + const conn = await getAiConnection("llm") + expect(conn.baseURL).toBe(`${GATEWAY_URL}/llm/v1`) + }) + + it("throws INVALID_BINDING_CONFIG on malformed JSON", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", "{not json") + await expect(getAiConnection("llm")).rejects.toMatchObject({ + code: "INVALID_BINDING_CONFIG", + }) + }) + + it("rejects an external binding with an unexpected key (strict at the trust boundary)", async () => { + vi.stubEnv( + "ALIEN_LLM_BINDING", + JSON.stringify({ service: "external", provider: "openai", apiKey: "sk", extra: "tampered" }), + ) + await expect(getAiConnection("llm")).rejects.toMatchObject({ + code: "INVALID_BINDING_CONFIG", + }) + }) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Ai client (against a BYO-key provider) — chat, streaming, models, errors +// ───────────────────────────────────────────────────────────────────────────── + +describe("Ai.chat.completions.create (non-streaming)", () => { + it("POSTs to the provider with the body unchanged and the BYO-key auth header", async () => { + const fetchMock = stubFetch({ id: "x", model: "gpt-4o", choices: [] }) + const params = { + model: "gpt-4o", + messages: [{ role: "user", content: "hi" }], + temperature: 0.5, + } + + await ai("llm").chat.completions.create(params) + + expect(callUrl(fetchMock)).toBe("https://api.openai.com/v1/chat/completions") + expect(callInit(fetchMock).method).toBe("POST") + expect(callBody(fetchMock)).toEqual(params) + const headers = (callInit(fetchMock).headers ?? {}) as Record + expect(headers.Authorization).toBe("Bearer sk-test") + }) + + it("canonicalizes the binding name to the ALIEN__BINDING env var", async () => { + vi.stubEnv("ALIEN_MY_LLM_BINDING", EXTERNAL) + const fetchMock = stubFetch({ id: "x", choices: [] }) + await ai("My-LLM").chat.completions.create({ model: "gpt-4o", messages: [] }) + expect(callUrl(fetchMock)).toBe("https://api.openai.com/v1/chat/completions") + }) + + it("POSTs an ambient binding through the gateway with a single /v1 and no key", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", JSON.stringify({ service: "bedrock", region: "us-east-2" })) + const fetchMock = stubFetch({ id: "x", choices: [] }) + await ai("llm").chat.completions.create({ model: "gpt-oss-20b", messages: [] }) + expect(callUrl(fetchMock)).toBe(`${GATEWAY_URL}/llm/v1/chat/completions`) + const headers = (callInit(fetchMock).headers ?? {}) as Record + expect(headers.Authorization).toBeUndefined() + }) + + it("throws an AiUpstreamError on a non-2xx response", async () => { + stubFetch({ error: { message: "boom" } }, 500) + await expect( + ai("llm").chat.completions.create({ model: "gpt-4o", messages: [] }), + ).rejects.toThrow(AlienError) + }) +}) + +describe("Ai.chat.completions.create (streaming)", () => { + it("returns an async iterable of SSE chunks when stream: true", async () => { + stubFetchSse([ + { choices: [{ delta: { content: "he" } }] }, + { choices: [{ delta: { content: "llo" } }] }, + ]) + const stream = (await ai("llm").chat.completions.create({ + model: "gpt-4o", + messages: [], + stream: true, + })) as AsyncIterable<{ choices: { delta: { content?: string } }[] }> + const parts: string[] = [] + for await (const chunk of stream) { + const c = chunk.choices[0]?.delta.content + if (c) parts.push(c) + } + expect(parts.join("")).toBe("hello") + }) +}) + +describe("Ai.getAvailableModels", () => { + it("returns a curated default for a BYO-key provider without hitting it", async () => { + const fetchMock = stubFetch({ data: [] }) + const models = await ai("llm").getAvailableModels() + expect(models.map(m => m.id)).toContain("gpt-4o-mini") + expect(fetchMock).not.toHaveBeenCalled() + }) + + it("fetches the gateway's curated catalog for an ambient binding", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", JSON.stringify({ service: "bedrock", region: "us-east-2" })) + const fetchMock = stubFetch({ data: [{ id: "gpt-oss-20b" }] }) + const models = await ai("llm").getAvailableModels() + expect(callUrl(fetchMock)).toBe(`${GATEWAY_URL}/llm/v1/models`) + expect(models).toEqual([{ id: "gpt-oss-20b" }]) + }) + + it("retries a transient gateway-start failure on a retained instance", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", JSON.stringify({ service: "bedrock", region: "us-east-2" })) + stubFetch({ data: [{ id: "gpt-oss-20b" }] }) + const start = vi + .fn() + .mockRejectedValueOnce(new Error("ambient credential unavailable")) + .mockResolvedValue({ url: GATEWAY_URL } as unknown as RawAiGatewayHandle) + const llm = createAiClient({ startAiGateway: start }).ai("llm") + + await expect(llm.getAvailableModels()).rejects.toThrow() + // A cached rejection would leave this instance permanently broken; it must retry. + expect(await llm.getAvailableModels()).toEqual([{ id: "gpt-oss-20b" }]) + expect(start).toHaveBeenCalledTimes(2) + }) +}) + +describe("Ai.responses.create", () => { + it("POSTs to the provider's /v1/responses with the body unchanged and the BYO-key auth header", async () => { + const fetchMock = stubFetch({ id: "resp_x", object: "response", output: [] }) + const params = { model: "gpt-4o", input: "hi" } + await ai("llm").responses.create(params) + expect(callUrl(fetchMock)).toBe("https://api.openai.com/v1/responses") + expect(callInit(fetchMock).method).toBe("POST") + expect(callBody(fetchMock)).toEqual(params) + const headers = (callInit(fetchMock).headers ?? {}) as Record + expect(headers.Authorization).toBe("Bearer sk-test") + }) + + it("POSTs an ambient binding through the gateway with a single /v1 and no key", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", JSON.stringify({ service: "bedrock", region: "us-east-2" })) + const fetchMock = stubFetch({ id: "resp_x", output: [] }) + await ai("llm").responses.create({ model: "gpt-oss-120b", input: "hi" }) + expect(callUrl(fetchMock)).toBe(`${GATEWAY_URL}/llm/v1/responses`) + const headers = (callInit(fetchMock).headers ?? {}) as Record + expect(headers.Authorization).toBeUndefined() + }) + + it("returns an async iterable of SSE chunks when stream: true", async () => { + stubFetchSse([ + { type: "response.output_text.delta", delta: "he" }, + { type: "response.output_text.delta", delta: "llo" }, + ]) + const stream = (await ai("llm").responses.create({ + model: "gpt-4o", + input: "hi", + stream: true, + })) as AsyncIterable<{ type: string; delta?: string }> + const parts: string[] = [] + for await (const chunk of stream) { + if (chunk.delta) parts.push(chunk.delta) + } + expect(parts.join("")).toBe("hello") + }) + + it("throws an AiUpstreamError on a non-2xx response", async () => { + stubFetch({ error: { message: "boom" } }, 500) + await expect(ai("llm").responses.create({ model: "gpt-4o", input: "hi" })).rejects.toThrow( + AlienError, + ) + }) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Ai.finetune / Ai.finetuneStatus — runtime fine-tuning (ambient gateway only) +// ───────────────────────────────────────────────────────────────────────────── + +const AMBIENT = JSON.stringify({ service: "bedrock", region: "us-east-2" }) + +describe("Ai.finetune", () => { + it("POSTs the training key to the gateway and returns { jobId, servedModel }", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", AMBIENT) + const fetchMock = stubFetch({ jobId: "job-123", servedModel: "llm-tuned" }) + + const result = await ai("llm").finetune({ trainingKey: "datasets/train.jsonl" }) + + expect(callUrl(fetchMock)).toBe(`${GATEWAY_URL}/llm/v1/finetune`) + expect(callInit(fetchMock).method).toBe("POST") + expect(callBody(fetchMock)).toEqual({ trainingKey: "datasets/train.jsonl" }) + // Ambient path injects the credential in the gateway; no client-side auth header. + const headers = (callInit(fetchMock).headers ?? {}) as Record + expect(headers.Authorization).toBeUndefined() + expect(result).toEqual({ jobId: "job-123", servedModel: "llm-tuned" }) + }) + + it("omits trainingKey from the body when not provided", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", AMBIENT) + const fetchMock = stubFetch({ jobId: "job-1", servedModel: "llm-tuned" }) + + await ai("llm").finetune() + + expect(callBody(fetchMock)).toEqual({}) + }) + + it("rejects for a BYO-key External binding without POSTing", async () => { + // EXTERNAL binding is stubbed in the top-level beforeEach. + const fetchMock = stubFetch({ jobId: "x", servedModel: "y" }) + await expect(ai("llm").finetune({ trainingKey: "k" })).rejects.toMatchObject({ + code: "INVALID_BINDING_CONFIG", + }) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it("throws an AiUpstreamError on a non-2xx gateway response", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", AMBIENT) + stubFetch({ error: { message: "boom" } }, 500) + await expect(ai("llm").finetune({ trainingKey: "k" })).rejects.toThrow(AlienError) + }) +}) + +describe("Ai.finetuneStatus", () => { + it("GETs the job by id and maps a succeeded status with the tuned model", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", AMBIENT) + const fetchMock = stubFetch({ status: "succeeded", model: "llm-tuned-v1" }) + + const status = await ai("llm").finetuneStatus("job-123") + + expect(callUrl(fetchMock)).toBe(`${GATEWAY_URL}/llm/v1/finetune/job-123`) + expect(callInit(fetchMock).method).toBe("GET") + expect(status).toEqual({ status: "succeeded", model: "llm-tuned-v1" }) + }) + + it("maps a running status", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", AMBIENT) + stubFetch({ status: "running" }) + expect(await ai("llm").finetuneStatus("job-1")).toEqual({ status: "running" }) + }) + + it("URL-encodes the job id", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", AMBIENT) + const fetchMock = stubFetch({ status: "running" }) + await ai("llm").finetuneStatus("arn:aws:bedrock/job 1") + expect(callUrl(fetchMock)).toBe( + `${GATEWAY_URL}/llm/v1/finetune/${encodeURIComponent("arn:aws:bedrock/job 1")}`, + ) + }) + + it("rejects for a BYO-key External binding without a GET", async () => { + const fetchMock = stubFetch({ status: "running" }) + await expect(ai("llm").finetuneStatus("job-1")).rejects.toMatchObject({ + code: "INVALID_BINDING_CONFIG", + }) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it("throws an AiUpstreamError on a non-2xx gateway response", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", AMBIENT) + stubFetch({ error: { message: "not found" } }, 404) + await expect(ai("llm").finetuneStatus("missing")).rejects.toThrow(AlienError) + }) +}) diff --git a/packages/ai-gateway/src/__tests__/gateway.test.ts b/packages/ai-gateway/src/__tests__/gateway.test.ts new file mode 100644 index 000000000..fcc1ae0ba --- /dev/null +++ b/packages/ai-gateway/src/__tests__/gateway.test.ts @@ -0,0 +1,57 @@ +import { AlienError } from "@alienplatform/core" +import { describe, expect, it, vi } from "vitest" + +import { createGateway } from "../gateway.js" +import type { NativeAddon, RawAiGatewayHandle } from "../loader.js" + +const handle = { url: "http://127.0.0.1:41999" } as unknown as RawAiGatewayHandle + +function addonThat(startAiGateway: NativeAddon["startAiGateway"]): NativeAddon { + return { startAiGateway, version: () => "1.11.2" } as unknown as NativeAddon +} + +describe("createGateway", () => { + it("starts the addon once and reuses the resolved handle", async () => { + const start = vi.fn().mockResolvedValue(handle) + const gateway = createGateway(() => addonThat(start)) + + expect(await gateway.startAiGateway()).toBe(handle) + expect(await gateway.startAiGateway()).toBe(handle) + expect(start).toHaveBeenCalledTimes(1) + }) + + it("retries after a transient failure instead of caching the rejection", async () => { + const start = vi + .fn() + .mockRejectedValueOnce(new Error("credential mint timed out")) + .mockResolvedValue(handle) + const gateway = createGateway(() => addonThat(start)) + + await expect(gateway.startAiGateway()).rejects.toThrow(AlienError) + // A cached rejection would leave the gateway permanently dead for this process. + expect(await gateway.startAiGateway()).toBe(handle) + expect(start).toHaveBeenCalledTimes(2) + }) + + it("rejects rather than throwing synchronously when the addon fails to load", async () => { + const gateway = createGateway(() => { + throw new Error("Cannot load the native addon for 'darwin-arm64'") + }) + // A synchronous throw would escape a caller's `.catch()`. + await expect(gateway.startAiGateway()).rejects.toThrow(AlienError) + }) + + it("decodes the addon's error envelope, preserving code and retryable", async () => { + const envelope = JSON.stringify({ + code: "GATEWAY_AMBIENT_CREDENTIAL_UNAVAILABLE", + message: "Could not obtain the workload's ambient cloud credential: metadata timeout", + retryable: true, + }) + const gateway = createGateway(() => addonThat(vi.fn().mockRejectedValue(new Error(envelope)))) + + await expect(gateway.startAiGateway()).rejects.toMatchObject({ + code: "GATEWAY_AMBIENT_CREDENTIAL_UNAVAILABLE", + retryable: true, + }) + }) +}) diff --git a/packages/ai-gateway/src/__tests__/loader.test.ts b/packages/ai-gateway/src/__tests__/loader.test.ts new file mode 100644 index 000000000..c6c4aacca --- /dev/null +++ b/packages/ai-gateway/src/__tests__/loader.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it } from "vitest" +import { + type NativeAddon, + loadAddon, + registerEmbeddedAddon, + resetAddonCacheForTests, +} from "../loader.js" + +describe("loadAddon embedded hatch", () => { + afterEach(() => { + resetAddonCacheForTests() + }) + + it("prefers a registered embedded addon over filesystem resolution", () => { + const embedded: NativeAddon = { + startAiGateway: async () => ({ url: "http://127.0.0.1:0" }), + version: () => "embedded-test", + } + // A `bun build --compile` binary registers its embedded addon up front (via + // the `/native` entry's installEmbeddedAddon). loadAddon must return it + // rather than probing the filesystem/prebuild, which cannot work inside the + // single-file binary. Registering a fake proves the embedded slot short- + // circuits resolution. + registerEmbeddedAddon(embedded) + expect(loadAddon()).toBe(embedded) + }) +}) diff --git a/packages/ai-gateway/src/alien-ai-gateway.d.node.ts b/packages/ai-gateway/src/alien-ai-gateway.d.node.ts new file mode 100644 index 000000000..ea065bd12 --- /dev/null +++ b/packages/ai-gateway/src/alien-ai-gateway.d.node.ts @@ -0,0 +1,9 @@ +// Type declaration for the statically-embedded native addon that `native.ts` +// imports via the literal specifier `./alien-ai-gateway.node`. No `.node` file +// exists at build time in this repo — the build stages it next to the built +// `native.js` (see PACKAGE_LAYOUT.md). This declaration lets `native.ts` +// typecheck and gives the default import the full addon type. +import type { NativeAddon } from "./loader.js" + +declare const addon: NativeAddon +export default addon diff --git a/packages/ai-gateway/src/binding.ts b/packages/ai-gateway/src/binding.ts new file mode 100644 index 000000000..1dcd59577 --- /dev/null +++ b/packages/ai-gateway/src/binding.ts @@ -0,0 +1,76 @@ +/** + * Parsing for the `ALIEN__BINDING` env var an `ai` resource projects. The client only + * routes on the binding: a BYO-key (`external`) binding is validated strictly here because the + * client itself uses its fields, while every other service tag — the ambient variants, including + * ones added to the platform after this SDK shipped — is passed through for the Rust gateway to + * validate and serve. Mirrors the Rust `AiBinding` (serde tag "service", lowercase, camelCase + * fields). + */ + +import { AlienError, InvalidBindingConfigError } from "@alienplatform/core" +import * as z from "zod/v4" + +// Strict: the external binding is the control-plane → workload trust boundary the client +// reads directly, so an unexpected key fails loudly rather than being silently dropped. +const externalAiBindingSchema = z + .object({ + service: z.literal("external"), + provider: z.string(), + apiKey: z.string(), + }) + .strict() + +export type ExternalAiBinding = z.infer + +/** A binding served by the gateway; `service` may be a variant this SDK predates. */ +export interface AmbientAiBinding { + service: string + [key: string]: unknown +} + +export type AiBinding = ExternalAiBinding | AmbientAiBinding + +/** Narrow an `AiBinding` to the BYO-key variant the client handles itself. */ +export function isExternalAiBinding(binding: AiBinding): binding is ExternalAiBinding { + return binding.service === "external" +} + +/** The env var an `ai` binding is projected into: `ALIEN__BINDING` (uppercased, `-`→`_`). */ +export function aiBindingEnvVarName(name: string): string { + return `ALIEN_${name.toUpperCase().replace(/-/g, "_")}_BINDING` +} + +/** Parse `ALIEN__BINDING`, or `undefined` if it is not set. Throws on malformed content. */ +export async function parseAiBinding(name: string): Promise { + const raw = process.env[aiBindingEnvVarName(name)] + if (!raw) return undefined + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch (cause) { + throw (await AlienError.from(cause)).withContext( + InvalidBindingConfigError.create({ message: `AI binding '${name}' is not valid JSON` }), + ) + } + const service = + parsed !== null && typeof parsed === "object" + ? (parsed as { service?: unknown }).service + : undefined + if (typeof service !== "string") { + throw new AlienError( + InvalidBindingConfigError.create({ + message: `AI binding '${name}' has no 'service' tag`, + }), + ) + } + if (service !== "external") { + return parsed as AmbientAiBinding + } + const result = externalAiBindingSchema.safeParse(parsed) + if (!result.success) { + throw (await AlienError.from(result.error)).withContext( + InvalidBindingConfigError.create({ message: `AI binding '${name}' has an unexpected shape` }), + ) + } + return result.data +} diff --git a/packages/ai-gateway/src/client.ts b/packages/ai-gateway/src/client.ts new file mode 100644 index 000000000..7742f7de0 --- /dev/null +++ b/packages/ai-gateway/src/client.ts @@ -0,0 +1,484 @@ +/** + * AI binding — a thin OpenAI-compatible client to the workload's AI gateway. + * + * For an ambient-cloud binding the in-process Rust gateway is started on the first call; for a + * BYO-key (External) binding the client talks to the provider directly. Either way the client + * only forwards OpenAI-shaped requests and streams responses back — it holds no ambient + * credentials and rewrites nothing (the gateway rewrites the model id and injects the + * credential). + */ + +import { AlienError } from "@alienplatform/core" + +import { isExternalAiBinding, parseAiBinding } from "./binding.js" +import { + AiTransportError, + AiUpstreamError, + BindingNotFoundError, + InvalidBindingConfigError, +} from "./errors.js" +import type { Gateway } from "./gateway.js" + +// ───────────────────────────────────────────────────────────────────────────── +// Public request / response types +// ───────────────────────────────────────────────────────────────────────────── + +export interface ChatCompletionCreateParams { + model: string + messages: Array<{ role: string; content: string | object }> + stream?: boolean + [key: string]: unknown +} + +export interface ResponseCreateParams { + model: string + input: string | Array<{ role: string; content: string }> + stream?: boolean + [key: string]: unknown +} + +/** One model the gateway exposes for this binding's cloud. */ +export interface AiModel { + id: string +} + +/** The handle returned when a runtime fine-tuning job is submitted through the gateway. */ +export interface FinetuneResult { + /** Gateway-assigned id used to poll the job's status. */ + jobId: string + /** The served model id inference should target once the job succeeds. */ + servedModel: string +} + +/** The status of a runtime fine-tuning job, as reported by the gateway. */ +export interface FinetuneJobStatus { + /** Lifecycle state of the job. */ + status: "running" | "succeeded" | "failed" + /** The tuned model id, present once the job has succeeded. */ + model?: string + /** A human-readable detail (e.g. the failure reason), when the gateway provides one. */ + message?: string +} + +// Upstream base URL (no `/v1`) for a BYO-key provider. `ALIEN_AI_LOCAL_BASE_URL` overrides it so +// any OpenAI-compatible provider works; unknown providers default to OpenAI's. +function providerBaseUrl(provider: string): string { + const override = process.env.ALIEN_AI_LOCAL_BASE_URL + if (override) return override.replace(/\/$/, "") + return provider === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com" +} + +// A small curated chat-model list for the BYO-key picker (we don't proxy the provider's own +// /v1/models, which returns hundreds of non-chat entries). +function defaultModels(provider: string): string[] { + return provider === "anthropic" + ? ["claude-3-5-sonnet-latest", "claude-3-5-haiku-latest"] + : ["gpt-4o-mini", "gpt-4o"] +} + +/** Resolution shared by `ai()` and `getAiConnection()`. `baseUrl` is the root (no `/v1`); + * `apiKey`/`staticModels` are set only for a BYO-key (External) provider. */ +export interface ResolvedAiBinding { + baseUrl: string + apiKey?: string + staticModels?: string[] +} + +/** + * Resolve `ALIEN__BINDING` to a connection target. External -> the provider directly with + * the projected key. Any other service tag (the ambient variants, including ones this SDK + * predates) -> the in-process Rust gateway, started here on demand, which validates the binding + * and injects the ambient cloud credential. Backs both `ai()` and `getAiConnection()`. + * + * The returned `baseUrl` is the root without `/v1` — the gateway serves + * `//v1/...`, and the client appends the versioned paths itself. + */ +export async function resolveAiBinding(gateway: Gateway, name: string): Promise { + const binding = await parseAiBinding(name) + if (!binding) { + throw new AlienError(BindingNotFoundError.create({ bindingName: name, bindingType: "Ai" })) + } + if (isExternalAiBinding(binding)) { + return { + baseUrl: providerBaseUrl(binding.provider), + apiKey: binding.apiKey, + staticModels: defaultModels(binding.provider), + } + } + const handle = await gateway.startAiGateway() + // Mirror the gateway's `canonical_binding_name` (lowercase, `_`->`-`) so the route key + // matches for every legal resource id, including underscored ones. + const segment = name.toLowerCase().replace(/_/g, "-") + return { baseUrl: `${handle.url}/${segment}` } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Upstream error constructor +// +// 429/502/503/504 are transient and safe to retry; all others are not. The gateway +// URL is loopback (not sensitive), but the error stays internal=true because the +// forwarded provider message may carry upstream detail. +// ───────────────────────────────────────────────────────────────────────────── + +const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]) + +function createUpstreamError(url: string, status: number, message: string): AlienError { + return new AlienError({ + ...AiUpstreamError.create({ url, status, message }).toOptions(), + // Retryability and the surfaced status vary per HTTP status, not by the + // definition, so override just those two on the schema-typed base. + retryable: RETRYABLE_STATUSES.has(status), + httpStatusCode: status >= 400 ? status : 502, + }) +} + +// ───────────────────────────────────────────────────────────────────────────── +// SSE streaming parser +// +// Reads from a Fetch API ReadableStream and yields each `data:` JSON payload, +// stopping at `data: [DONE]`. Chunks pass through unchanged — the gateway already +// returns the upstream's native stream. Uses getReader() rather than `for await` +// because web ReadableStream is not reliably async-iterable in all runtimes (Bun). +// ───────────────────────────────────────────────────────────────────────────── + +async function* parseSse( + url: string, + body: ReadableStream, +): AsyncGenerator> { + const reader = body.getReader() + const decoder = new TextDecoder() + let buffer = "" + + const emit = function* (line: string): Generator> { + const trimmed = line.trimEnd() + if (!trimmed.startsWith("data:")) return + const payload = trimmed.slice(5).trim() + if (payload === "[DONE]") return + let chunk: Record + try { + chunk = JSON.parse(payload) as Record + } catch (cause) { + throw new AlienError({ + ...AiTransportError.create({ + url, + reason: `Malformed SSE chunk: ${cause instanceof Error ? cause.message : String(cause)}`, + }).toOptions(), + source: { + code: "GENERIC_ERROR", + message: cause instanceof Error ? (cause.stack ?? cause.message) : String(cause), + retryable: false, + internal: true, + }, + }) + } + yield chunk + } + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() ?? "" + + for (const line of lines) { + if (line.trimEnd() === "data: [DONE]") return + yield* emit(line) + } + } + + // Flush any bytes still held by the decoder, then any trailing line. + buffer += decoder.decode() + for (const line of buffer.split("\n")) { + if (line.trimEnd() === "data: [DONE]") return + yield* emit(line) + } + } finally { + // cancel() both cancels the stream and releases the reader lock. + await reader.cancel().catch(() => {}) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Ai class +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Constructed by the `ai(name)` factory; resolves the binding (and starts the + * gateway, for ambient) once on first use. + * + * @example + * ```typescript + * import { ai } from "@alienplatform/sdk" + * + * const llm = ai("my-llm") + * const response = await llm.chat.completions.create({ + * model: "claude-opus-4.8", + * messages: [{ role: "user", content: "Hello!" }], + * }) + * ``` + */ +export class Ai { + private readonly resolve: () => Promise + private connectionPromise: Promise | null = null + + /** OpenAI-compatible chat namespace. */ + readonly chat: { + completions: { + create(params: ChatCompletionCreateParams): Promise + } + } + + /** OpenAI-compatible Responses API namespace (the surface Codex speaks). */ + readonly responses: { + create(params: ResponseCreateParams): Promise + } + + constructor(resolve: () => Promise) { + this.resolve = resolve + + this.chat = { + completions: { + create: (params: ChatCompletionCreateParams) => this._chatCompletionsCreate(params), + }, + } + + this.responses = { + create: (params: ResponseCreateParams) => this._responsesCreate(params), + } + } + + // Resolve the binding (and start the gateway for an ambient one) once, then reuse. + // Only a resolved connection is memoized: caching a rejection would leave a retained + // instance permanently broken after one transient gateway-start failure, which the Rust + // side reports as retryable — the same guarantee `createGateway` keeps. + private connection(): Promise { + this.connectionPromise ??= this.resolve().catch(error => { + this.connectionPromise = null + throw error + }) + return this.connectionPromise + } + + /** List the models the gateway exposes for this binding's cloud. */ + async getAvailableModels(): Promise { + const { baseUrl, staticModels } = await this.connection() + // Curated default for a BYO-key provider (see `defaultModels`); the gateway + // path fetches the cloud's catalog below. + if (staticModels) { + return staticModels.map(id => ({ id })) + } + const url = `${baseUrl}/v1/models` + const response = await this._fetch(url, { method: "GET" }) + if (!response.ok) { + throw createUpstreamError(url, response.status, await extractErrorMessage(response)) + } + let body: { data?: AiModel[] } + try { + body = (await response.json()) as { data?: AiModel[] } + } catch (jsonError) { + throw (await AlienError.from(jsonError)).withContext( + AiTransportError.create({ + url, + reason: `Response body is not valid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`, + }), + ) + } + // The gateway always returns a `data` array; its absence means a broken response. + if (!Array.isArray(body.data)) { + throw createUpstreamError(url, response.status, "models response had no data array") + } + return body.data + } + + /** + * Submit a runtime fine-tuning job for this binding's model. Only the ambient (gateway) + * path supports fine-tuning — a BYO-key External binding has no gateway finetune endpoint, + * so this rejects rather than POSTing to the raw provider. + */ + async finetune(opts?: { trainingKey?: string }): Promise { + const { baseUrl, apiKey } = await this.connection() + if (apiKey) throw finetuneUnsupportedError() + + const url = `${baseUrl}/v1/finetune` + // Omit trainingKey entirely when not supplied; the capability already carries a default key. + const body = opts?.trainingKey === undefined ? {} : { trainingKey: opts.trainingKey } + const response = await this._fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + if (!response.ok) { + throw createUpstreamError(url, response.status, await extractErrorMessage(response)) + } + return this._parseJson(url, response) + } + + /** Poll a runtime fine-tuning job by its id. Ambient (gateway) path only. */ + async finetuneStatus(jobId: string): Promise { + const { baseUrl, apiKey } = await this.connection() + if (apiKey) throw finetuneUnsupportedError() + + const url = `${baseUrl}/v1/finetune/${encodeURIComponent(jobId)}` + const response = await this._fetch(url, { method: "GET" }) + if (!response.ok) { + throw createUpstreamError(url, response.status, await extractErrorMessage(response)) + } + return this._parseJson(url, response) + } + + // Parse a JSON response body, wrapping a parse failure in an AiTransportError like the + // other JSON-returning methods (getAvailableModels, _postSurface) do. + private async _parseJson(url: string, response: Response): Promise { + try { + return (await response.json()) as T + } catch (jsonError) { + throw (await AlienError.from(jsonError)).withContext( + AiTransportError.create({ + url, + reason: `Response body is not valid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`, + }), + ) + } + } + + private _chatCompletionsCreate(params: ChatCompletionCreateParams): Promise { + return this._postSurface("/v1/chat/completions", params) + } + + private _responsesCreate(params: ResponseCreateParams): Promise { + return this._postSurface("/v1/responses", params) + } + + // The chat-completions and responses surfaces are byte-for-byte passthroughs that differ + // only in path, so they share one POST + stream/JSON handler. The gateway (or provider) + // returns the upstream's native body unchanged. + private async _postSurface( + path: string, + params: { stream?: boolean } & Record, + ): Promise { + const { baseUrl, apiKey } = await this.connection() + const url = `${baseUrl}${path}` + const response = await this._fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + // Only the BYO-key path authenticates here; ambient credentials are the gateway's. + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + }, + body: JSON.stringify(params), + }) + + if (!response.ok) { + throw createUpstreamError(url, response.status, await extractErrorMessage(response)) + } + + if (params.stream === true) { + if (!response.body) { + throw createUpstreamError(url, response.status, "Streaming response body is null") + } + return parseSse(url, response.body) + } + + try { + return await response.json() + } catch (jsonError) { + throw (await AlienError.from(jsonError)).withContext( + AiTransportError.create({ + url, + reason: `Response body is not valid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`, + }), + ) + } + } + + private async _fetch(url: string, init: RequestInit): Promise { + try { + return await fetch(url, init) + } catch (fetchError) { + throw (await AlienError.from(fetchError)).withContext( + AiTransportError.create({ + url, + reason: fetchError instanceof Error ? fetchError.message : String(fetchError), + }), + ) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Entry wiring +// ───────────────────────────────────────────────────────────────────────────── + +/** + * A resolved AI connection for a plain OpenAI-compatible client: the base URL, plus an + * optional bearer key. `apiKey` is set for a BYO-key (External) provider and omitted for + * ambient-cloud bindings, where the embedded gateway injects the credential. + */ +export interface AiConnection { + baseURL: string + apiKey?: string +} + +/** The app-facing AI client surface, shared by the lazy-loading and static-embed entries. */ +export interface AiClient { + ai(name: string): Ai + getAiConnection(name: string): Promise +} + +/** + * Build the `ai()` / `getAiConnection()` pair over a gateway. Mirrors `createGateway`: + * each entry wires its own addon acquisition and shares this implementation. + */ +export function createAiClient(gateway: Gateway): AiClient { + return { + /** An OpenAI-compatible client for the named AI binding. */ + ai(name: string): Ai { + return new Ai(() => resolveAiBinding(gateway, name)) + }, + + /** + * Resolve an AI binding to `{ baseURL, apiKey? }` for a plain OpenAI-compatible client + * (e.g. the Vercel AI SDK's `createOpenAICompatible`). The binding decides the target, + * so app code is identical whether AI is a BYO-key provider (local) or an ambient-cloud + * model behind the gateway. + * + * Awaits gateway startup for an ambient binding, so the returned `baseURL` is live + * before the caller's client uses it. + */ + async getAiConnection(name: string): Promise { + const resolved = await resolveAiBinding(gateway, name) + return { baseURL: `${resolved.baseUrl}/v1`, apiKey: resolved.apiKey } + }, + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── + +// Fine-tuning is a capability of the ambient gateway; a BYO-key (External) provider is reached +// directly and has no gateway finetune endpoint, so the operation is unsupported there. +function finetuneUnsupportedError() { + return new AlienError( + InvalidBindingConfigError.create({ + message: "Fine-tuning is not supported for BYO-key (External) AI providers", + suggestion: "Use an ambient-cloud AI binding with a fine-tune capability configured", + }), + ) +} + +async function extractErrorMessage(response: Response): Promise { + try { + const errBody = (await response.json()) as Record + const errObj = errBody.error + if (errObj && typeof errObj === "object" && "message" in errObj) { + return String((errObj as Record).message) + } + return errBody.message ? String(errBody.message) : response.statusText + } catch { + return response.statusText || "Unknown upstream error" + } +} diff --git a/packages/ai-gateway/src/errors.ts b/packages/ai-gateway/src/errors.ts new file mode 100644 index 000000000..45b3a4cc6 --- /dev/null +++ b/packages/ai-gateway/src/errors.ts @@ -0,0 +1,86 @@ +/** + * Typed errors for the AI client surface (`ai()` / `getAiConnection`). + * + * Defined here rather than in `@alienplatform/bindings` because the AI binding + * resolves through this package's gateway, not the bindings native addon. + */ + +import { defineError } from "@alienplatform/core" +import * as z from "zod/v4" + +// Shared with the binding surfaces in @alienplatform/bindings. +export { BindingNotFoundError, InvalidBindingConfigError } from "@alienplatform/core" + +/** + * Error thrown when an upstream LLM endpoint returns a non-2xx response. + * + * Marked internal because the response body and upstream URL may contain + * provider-specific infra detail not intended for external clients. + * Retryability is derived per-call from the HTTP status code. + */ +export const AiUpstreamError = defineError({ + code: "AI_UPSTREAM_ERROR", + context: z.object({ + url: z.string(), + status: z.number(), + message: z.string(), + }), + message: ({ url, status, message }) => + `AI upstream request to '${url}' failed with status ${status}: ${message}`, + retryable: false, + internal: true, + httpStatusCode: 502, +}) + +/** + * Error thrown when a network-level failure occurs before or after an + * upstream LLM request (fetch threw, or the success body was not parseable JSON). + * These are always transient and safe to retry. + */ +export const AiTransportError = defineError({ + code: "AI_TRANSPORT_ERROR", + context: z.object({ + url: z.string(), + reason: z.string(), + }), + message: ({ url, reason }) => `AI request to '${url}' failed due to a transport error: ${reason}`, + retryable: true, + internal: true, + httpStatusCode: 503, +}) + +/** + * Error thrown when this platform/architecture has no native addon. + */ +export const UnsupportedPlatformError = defineError({ + code: "AI_GATEWAY_UNSUPPORTED_PLATFORM", + context: z.object({ + platform: z.string(), + arch: z.string(), + reason: z.string().optional(), + }), + message: ({ platform, arch, reason }) => + `@alienplatform/ai-gateway has no native addon for platform '${platform}' arch '${arch}'${reason ? `: ${reason}` : ""}.`, + retryable: false, + internal: false, + httpStatusCode: 400, +}) + +/** + * Error thrown when the native addon exists but could not be loaded. Internal because the + * context carries filesystem paths. + */ +export const NativeAddonLoadFailedError = defineError({ + code: "AI_GATEWAY_ADDON_LOAD_FAILED", + context: z.object({ + triple: z.string(), + reason: z.string(), + path: z.string().optional(), + }), + message: ({ triple, reason }) => + `Cannot load the @alienplatform/ai-gateway native addon for '${triple}': ${reason}`, + retryable: false, + internal: true, + // Same class as UnsupportedPlatformError: the host environment can't run the addon. + httpStatusCode: 400, +}) diff --git a/packages/ai-gateway/src/gateway.ts b/packages/ai-gateway/src/gateway.ts new file mode 100644 index 000000000..5f94bcfc0 --- /dev/null +++ b/packages/ai-gateway/src/gateway.ts @@ -0,0 +1,31 @@ +/** + * The gateway surface, parameterized over how the native addon is obtained, so the + * lazy-loading entry (`index.ts`) and the static-embed entry (`native.ts`) share + * one implementation — mirroring `@alienplatform/bindings`' `createFactories`. + */ + +import type { NativeAddon, RawAiGatewayHandle } from "./loader.js" +import { unwrapNapiError } from "./napi-error.js" + +export interface Gateway { + startAiGateway(): Promise +} + +export function createGateway(getAddon: () => NativeAddon): Gateway { + // Started once per process: the handle is held for the process lifetime, since dropping it + // aborts the Rust server. Only a *resolved* start is memoized — caching a rejection would + // turn one transient credential failure into a permanently dead gateway, even though the + // Rust side marks those errors retryable. + let started: Promise | null = null + + // `async` so a caller's `.catch()` sees a rejection: loading the addon throws synchronously. + async function startAiGateway(): Promise { + started ??= (async () => getAddon().startAiGateway())().catch(error => { + started = null + throw unwrapNapiError(error) + }) + return started + } + + return { startAiGateway } +} diff --git a/packages/ai-gateway/src/index.ts b/packages/ai-gateway/src/index.ts new file mode 100644 index 000000000..9e0a664dc --- /dev/null +++ b/packages/ai-gateway/src/index.ts @@ -0,0 +1,50 @@ +/** + * `@alienplatform/ai-gateway` — the thin TypeScript wrapper that starts the Alien + * AI gateway in-process. + * + * The gateway itself is a single Rust implementation (the `alien-gateway` crate) + * compiled to a napi-rs addon. This package loads that addon and starts it once + * per process, returning the loopback base URL. The app then points a plain + * OpenAI-compatible client at that URL, and every request and SSE stream flows + * over the loopback HTTP socket straight into the Rust gateway — no gateway logic + * is reimplemented here, and nothing crosses the napi boundary per request. + * + * @example + * ```typescript + * import { getAiConnection } from "@alienplatform/ai-gateway" + * import { createOpenAICompatible } from "@ai-sdk/openai-compatible" + * const provider = createOpenAICompatible({ name: "alien", ...(await getAiConnection("llm")) }) + * ``` + */ + +import { createAiClient } from "./client.js" +import { createGateway } from "./gateway.js" +import { loadAddon } from "./loader.js" + +const gateway = createGateway(loadAddon) +const client = createAiClient(gateway) + +/** Start the in-process AI gateway (idempotent) and return its running handle. */ +export const startAiGateway = gateway.startAiGateway +/** An OpenAI-compatible client for the named AI binding (External BYO-key or ambient). */ +export const ai = client.ai +/** Resolve an AI binding to `{ baseURL, apiKey? }`, starting the gateway for ambient bindings. */ +export const getAiConnection = client.getAiConnection + +export { Ai } from "./client.js" +export type { + AiConnection, + AiModel, + ChatCompletionCreateParams, + FinetuneJobStatus, + FinetuneResult, + ResponseCreateParams, +} from "./client.js" +export { aiBindingEnvVarName, isExternalAiBinding, parseAiBinding } from "./binding.js" +export type { AiBinding, AmbientAiBinding, ExternalAiBinding } from "./binding.js" +export { + AiTransportError, + AiUpstreamError, + BindingNotFoundError, + InvalidBindingConfigError, +} from "./errors.js" diff --git a/packages/ai-gateway/src/loader.ts b/packages/ai-gateway/src/loader.ts new file mode 100644 index 000000000..f0607b602 --- /dev/null +++ b/packages/ai-gateway/src/loader.ts @@ -0,0 +1,226 @@ +/** + * Native addon loader for the AI gateway. + * + * Resolves the napi-rs addon for the current platform, in order (mirrors + * `@alienplatform/bindings`): + * + * 1. `ALIEN_AI_GATEWAY_ADDON_PATH` — an explicit path to a `.node` file + * (dev/test escape hatch; never set in published installs). + * 2. The per-platform prebuild package `@alienplatform/ai-gateway-` + * from `optionalDependencies` (injected at publish time; absent in dev). + * 3. Dev fallback: the locally-built addon under + * `crates/alien-ai-gateway-node/alien-ai-gateway-node..node`, found + * by walking up from this module, version-gated against this package. + * + * Loading is deferred to first use, so importing the package performs no I/O. + */ + +import { existsSync } from "node:fs" +import { createRequire } from "node:module" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" +import { AlienError } from "@alienplatform/core" + +import { NativeAddonLoadFailedError, UnsupportedPlatformError } from "./errors.js" + +const require = createRequire(import.meta.url) + +/** A running gateway handle from the addon: its loopback base URL. Held for the + * process lifetime — dropping the last reference stops the server. */ +export interface RawAiGatewayHandle { + readonly url: string +} + +/** The complete napi addon surface consumed by the wrapper. */ +export interface NativeAddon { + startAiGateway(): Promise + version(): string +} + +export type LinuxLibc = "gnu" | "musl" + +/** Detect glibc vs musl, the same way napi-rs's generated loader does. */ +export function detectLinuxLibc(): LinuxLibc { + const report = + typeof process.report?.getReport === "function" + ? (process.report.getReport() as { header?: { glibcVersionRuntime?: string } }) + : undefined + if (report?.header) { + return report.header.glibcVersionRuntime ? "gnu" : "musl" + } + return existsSync("/etc/alpine-release") ? "musl" : "gnu" +} + +/** Map `process.platform`/`arch` to the napi triple (prebuild name + `.node` file name). */ +export function platformTriple( + platform: NodeJS.Platform = process.platform, + arch: NodeJS.Architecture = process.arch, + libc: LinuxLibc = platform === "linux" ? detectLinuxLibc() : "gnu", +): string { + if (platform === "darwin" && arch === "arm64") return "darwin-arm64" + if (platform === "darwin" && arch === "x64") return "darwin-x64" + if (platform === "linux" && libc === "musl") { + throw new AlienError( + UnsupportedPlatformError.create({ + platform, + arch, + reason: + "prebuilds are published for glibc Linux only; run on a glibc-based image (debian/ubuntu-slim)", + }), + ) + } + if (platform === "linux" && arch === "x64") return "linux-x64-gnu" + if (platform === "linux" && arch === "arm64") return "linux-arm64-gnu" + throw new AlienError(UnsupportedPlatformError.create({ platform, arch })) +} + +/** Walk up from `startDir` to find the locally-built addon, or `undefined`. */ +export function findLocalAddon( + triple: string, + startDir: string = dirname(fileURLToPath(import.meta.url)), +): string | undefined { + const fileName = `alien-ai-gateway-node.${triple}.node` + let dir = startDir + for (;;) { + const candidate = join(dir, "crates", "alien-ai-gateway-node", fileName) + if (existsSync(candidate)) return candidate + const parent = dirname(dir) + if (parent === dir) return undefined + dir = parent + } +} + +function packageVersion(): string { + const dir = dirname(fileURLToPath(import.meta.url)) + const packageJson = require(join(dir, "..", "package.json")) as { version: string } + return packageJson.version +} + +let cached: NativeAddon | undefined +let embedded: NativeAddon | undefined + +/** + * Register a bun-embedded addon with the default loader, so plain + * `@alienplatform/ai-gateway` imports (which go through {@link loadAddon}) + * resolve to it inside a `bun build --compile` binary — where the + * filesystem/prebuild resolution below cannot find the addon. In a normal + * install this is never called and {@link loadAddon} falls through to its + * normal resolution. The `/native` entry calls this at bootstrap. + */ +export function registerEmbeddedAddon(addon: NativeAddon): void { + embedded = addon +} + +/** Load (and memoize) the native addon, or throw if none resolves for this platform. */ +export function loadAddon(): NativeAddon { + if (cached) return cached + + // A compiled binary registers its embedded addon up front; prefer it over the + // filesystem/prebuild resolution below, which cannot work inside the binary. + if (embedded) { + cached = embedded + return cached + } + + const triple = platformTriple() + const pkg = `@alienplatform/ai-gateway-${triple}` + + const override = process.env.ALIEN_AI_GATEWAY_ADDON_PATH + if (override) { + // Resolved against cwd, not this module's dist/ directory. + cached = requireAddon(resolve(override), triple, "ALIEN_AI_GATEWAY_ADDON_PATH") + return cached + } + + const prebuild = requirePrebuild(pkg, triple) + if (prebuild) { + cached = prebuild + return cached + } + + const local = findLocalAddon(triple) + if (local) { + const addon = requireAddon(local, triple, "the locally-built addon") + const expected = packageVersion() + const actual = addon.version() + // Trust the local addon only when its reported version matches the installed package + // version. A mismatch is a stale build from an earlier checkout (ABI/version skew). + if (actual === expected) { + cached = addon + return cached + } + throw new AlienError( + NativeAddonLoadFailedError.create({ + triple, + path: local, + reason: `the locally-built addon reports version '${actual}', but this package is '${expected}' — rebuild it with \`napi build --platform\` in crates/alien-ai-gateway-node`, + }), + ) + } + + throw new AlienError( + NativeAddonLoadFailedError.create({ + triple, + reason: `no addon found — install the '${pkg}' prebuild, build it locally with \`napi build --platform\` in crates/alien-ai-gateway-node, or set ALIEN_AI_GATEWAY_ADDON_PATH to a built .node file`, + }), + ) +} + +/** `require` the prebuild, or `undefined` when it is not installed. */ +// Preserve the caught `require()` error as a structured source. This path is synchronous +// (CJS require) and so cannot use the async `AlienError.from`; building the source inline keeps +// the original dlopen/ABI failure (stack included) from being lost behind the generic message. +function nativeAddonLoadFailure( + triple: string, + path: string, + reason: string, + cause: unknown, +): AlienError { + return new AlienError({ + ...NativeAddonLoadFailedError.create({ triple, path, reason }).toOptions(), + source: { + code: "GENERIC_ERROR", + message: cause instanceof Error ? (cause.stack ?? cause.message) : String(cause), + retryable: false, + internal: true, + }, + }) +} + +function requirePrebuild(pkg: string, triple: string): NativeAddon | undefined { + try { + return require(pkg) as NativeAddon + } catch (error) { + // Only "not installed" falls through to the dev-built addon. Anything else — a failed + // dlopen from ABI skew, a corrupt .node, a broken transitive require — is a real + // failure that the generic "install the prebuild" advice would misdiagnose. + if ((error as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") { + return undefined + } + throw nativeAddonLoadFailure( + triple, + pkg, + error instanceof Error ? error.message : String(error), + error, + ) + } +} + +function requireAddon(path: string, triple: string, source: string): NativeAddon { + try { + return require(path) as NativeAddon + } catch (error) { + throw nativeAddonLoadFailure( + triple, + path, + `${source} could not be loaded: ${error instanceof Error ? error.message : String(error)}`, + error, + ) + } +} + +/** Test-only: reset the memoized addon. */ +export function resetAddonCacheForTests(): void { + cached = undefined + embedded = undefined +} diff --git a/packages/ai-gateway/src/napi-error.ts b/packages/ai-gateway/src/napi-error.ts new file mode 100644 index 000000000..8415aa375 --- /dev/null +++ b/packages/ai-gateway/src/napi-error.ts @@ -0,0 +1,85 @@ +/** + * Recovers the addon's error envelope. The Rust side serializes + * `{ code, message, context?, retryable, internal }` into the napi error's message; without + * this decoder the caller sees a raw JSON blob as `Error.message` and the envelope's + * `retryable`/`internal` flags — the whole point of carrying them across the boundary — are + * unreachable. + * + * Mirrors `unwrapNapiError` in `@alienplatform/bindings` (which does not yet carry `internal`). + */ + +import { AlienError } from "@alienplatform/core" + +/** Fallback code for napi-internal errors whose message is not an envelope. */ +const GENERIC_GATEWAY_CODE = "AI_GATEWAY_ERROR" + +/** The structured payload the addon serializes into `err.message`. */ +interface NapiErrorEnvelope { + code: string + message: string + context?: Record + retryable?: boolean + internal?: boolean + httpStatusCode?: number + hint?: string +} + +/** + * Parse the addon error envelope out of `err.message`. `undefined` for non-JSON messages + * (napi-internal errors such as a failed addon load) or JSON lacking a string `code`. + */ +function parseEnvelope(rawMessage: string): NapiErrorEnvelope | undefined { + let parsed: unknown + try { + parsed = JSON.parse(rawMessage) + } catch { + return undefined + } + if ( + parsed !== null && + typeof parsed === "object" && + typeof (parsed as { code?: unknown }).code === "string" + ) { + return parsed as NapiErrorEnvelope + } + return undefined +} + +/** + * Recover a typed {@link AlienError} from an error thrown by the native addon, preserving + * the envelope's `code`, `message`, `context`, and `retryable` flag. An error that is + * already an `AlienError` passes through; a non-envelope message is wrapped generically. + */ +export function unwrapNapiError(err: unknown): AlienError { + if (err instanceof AlienError) { + return err + } + + const rawMessage = err instanceof Error ? err.message : String(err) + const envelope = parseEnvelope(rawMessage) + + if (!envelope) { + // A non-envelope message is a napi-internal failure (panic, addon load) whose + // raw text may carry sensitive detail — fail closed on redaction. + return new AlienError({ + code: GENERIC_GATEWAY_CODE, + message: rawMessage, + retryable: false, + internal: true, + }) + } + + return new AlienError({ + code: envelope.code, + message: envelope.message ?? rawMessage, + retryable: envelope.retryable ?? false, + // Honor the Rust error's redaction posture; default closed if a pre-`internal` + // addon omits it. + internal: envelope.internal ?? true, + context: envelope.context ?? {}, + // Carry the Rust error's status code through rather than defaulting to 500, + // so a startup config error (e.g. 400) is not rendered as a server fault. + httpStatusCode: envelope.httpStatusCode, + hint: envelope.hint, + }) +} diff --git a/packages/ai-gateway/src/native.ts b/packages/ai-gateway/src/native.ts new file mode 100644 index 000000000..4aa697503 --- /dev/null +++ b/packages/ai-gateway/src/native.ts @@ -0,0 +1,52 @@ +/** + * `@alienplatform/ai-gateway/native` — static-embed entry for `bun build --compile`. + * + * Imports the native addon through the literal specifier `./alien-ai-gateway.node` + * so bun's compiler can detect the reference and embed it into the single-file + * executable. Unlike the default entry it does not probe the filesystem — the + * addon must already be staged next to the built `native.js` (the build owns that + * copy step). The specifier is kept external at build time (tsdown.config.ts) so + * the literal survives into `dist/native.js`. `bun build --compile` needs + * `--format=cjs`. + */ + +import addon from "./alien-ai-gateway.node" +import { createAiClient } from "./client.js" +import { createGateway } from "./gateway.js" +import { registerEmbeddedAddon } from "./loader.js" + +/** + * Register the bun-embedded addon with the default loader, so plain + * `@alienplatform/ai-gateway` imports — including the SDK's re-exported `ai()`, + * which resolves through {@link loadAddon} — use it inside a compiled binary. + * `alien build` emits an explicit call to this from the compiled entry (via + * `@alienplatform/sdk/native`); it's an explicit exported call, not a bare + * side-effect import, so it survives this package's `sideEffects: false` + * tree-shaking. + */ +export function installEmbeddedAddon(): void { + registerEmbeddedAddon(addon) +} + +const gateway = createGateway(() => addon) +const client = createAiClient(gateway) + +export const startAiGateway = gateway.startAiGateway +export const ai = client.ai +export const getAiConnection = client.getAiConnection + +export { Ai } from "./client.js" +export type { + AiConnection, + AiModel, + ChatCompletionCreateParams, + ResponseCreateParams, +} from "./client.js" +export { aiBindingEnvVarName, isExternalAiBinding, parseAiBinding } from "./binding.js" +export type { AiBinding, AmbientAiBinding, ExternalAiBinding } from "./binding.js" +export { + AiTransportError, + AiUpstreamError, + BindingNotFoundError, + InvalidBindingConfigError, +} from "./errors.js" diff --git a/packages/ai-gateway/tsconfig.build.json b/packages/ai-gateway/tsconfig.build.json new file mode 100644 index 000000000..741af23aa --- /dev/null +++ b/packages/ai-gateway/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "exclude": ["node_modules", "src/**/__tests__/**", "tests/**", "scripts/**"] +} diff --git a/packages/ai-gateway/tsconfig.json b/packages/ai-gateway/tsconfig.json new file mode 100644 index 000000000..b626eb5ee --- /dev/null +++ b/packages/ai-gateway/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@alienplatform/typescript-config/base.json", + "compilerOptions": { + "allowArbitraryExtensions": true, + "allowImportingTsExtensions": true + }, + "include": ["src/**/*", "tests/**/*", "scripts/**/*.ts"] +} diff --git a/packages/ai-gateway/tsdown.config.ts b/packages/ai-gateway/tsdown.config.ts new file mode 100644 index 000000000..ddc438adc --- /dev/null +++ b/packages/ai-gateway/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsdown" + +export default defineConfig({ + entry: ["src/index.ts", "src/native.ts"], + // Declarations are emitted by `tsc --emitDeclarationOnly` (see package.json). + dts: false, + hash: false, + ignoreWatch: ".turbo", + // Never bundle the native addon: the `./alien-ai-gateway.node` specifier in + // native.ts must survive into the output as a literal so bun can embed it. + external: [/\.node$/], +}) diff --git a/packages/bindings/src/errors.ts b/packages/bindings/src/errors.ts index 477f88c05..547343992 100644 --- a/packages/bindings/src/errors.ts +++ b/packages/bindings/src/errors.ts @@ -130,3 +130,6 @@ export function unwrapNapiError(err: unknown): AlienError { context, }) } + +// Shared with the AI binding surface in @alienplatform/ai-gateway. +export { BindingNotFoundError } from "@alienplatform/core" diff --git a/packages/bindings/src/index.ts b/packages/bindings/src/index.ts index 4c18155d8..c4bcf4c17 100644 --- a/packages/bindings/src/index.ts +++ b/packages/bindings/src/index.ts @@ -25,7 +25,12 @@ export const vault = factories.vault /** Resolve the linked-container binding named `name`. */ export const container = factories.container -export { AlienError, BindingNotConfiguredError, defineError } from "./errors.js" +export { + AlienError, + BindingNotConfiguredError, + BindingNotFoundError, + defineError, +} from "./errors.js" export type { Container, diff --git a/packages/core/src/__tests__/ai.test.ts b/packages/core/src/__tests__/ai.test.ts new file mode 100644 index 000000000..683b517b1 --- /dev/null +++ b/packages/core/src/__tests__/ai.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest" +import { AI } from "../ai.js" +import { Storage } from "../storage.js" + +describe("AI", () => { + it("builds with just an id", () => { + const r = new AI("llm").build() + expect(r.config.type).toBe("ai") + expect(r.config.id).toBe("llm") + }) + + // BYO-key external providers are NOT declared on the resource; they are + // supplied at deploy time as an ExternalBinding::Ai. The resource carries + // only its id, so there is no `external` field on the built config. + it("does not carry an external field", () => { + const r = new AI("llm").build() + expect(r.config).not.toHaveProperty("external") + }) + + // A stray `external` config must not survive the schema parse, so it can never reach the resource. + it("strips a stray external field at build time", () => { + const ai = new AI("llm") + ;(ai as unknown as { _config: Record })._config.external = { + provider: "openai", + } + const r = ai.build() + expect(r.config).not.toHaveProperty("external") + expect(r.config.id).toBe("llm") + }) + + it("omits finetune by default", () => { + const r = new AI("llm").build() + expect(r.config).not.toHaveProperty("finetune") + }) + + it("accepts a Storage resource for trainingData and resolves it to its id", () => { + const dataset = new Storage("training-set").build() + const r = new AI("llm") + .finetune({ baseModel: "amazon.nova-lite-v1:0", trainingData: dataset }) + .build() + const finetune = (r.config as { finetune?: Record }).finetune + expect(finetune).toBeDefined() + expect(finetune?.baseModel).toBe("amazon.nova-lite-v1:0") + expect(finetune?.trainingData).toBe("training-set") + }) + + it("accepts a string id for trainingData and carries all fields", () => { + const r = new AI("llm") + .finetune({ + baseModel: "amazon.nova-lite-v1:0", + trainingData: "training-set", + trainingKey: "data.jsonl", + servedModelId: "finance-model", + method: "lora", + }) + .build() + const finetune = (r.config as { finetune?: Record }).finetune + expect(finetune).toEqual({ + baseModel: "amazon.nova-lite-v1:0", + trainingData: "training-set", + trainingKey: "data.jsonl", + servedModelId: "finance-model", + method: "lora", + }) + }) + + it("is chainable and returns the same builder", () => { + const ai = new AI("llm") + expect(ai.finetune({ baseModel: "b", trainingData: "d" })).toBe(ai) + }) +}) diff --git a/packages/core/src/ai.ts b/packages/core/src/ai.ts new file mode 100644 index 000000000..49e468709 --- /dev/null +++ b/packages/core/src/ai.ts @@ -0,0 +1,96 @@ +import { + type Ai as AiConfig, + AiSchema, + type FinetuneMethod, + type ResourceType, +} from "./generated/index.js" +import { Resource } from "./resource.js" + +export type { AiOutputs, Ai as AiConfig, FinetuneSpec, FinetuneMethod } from "./generated/index.js" +export { AiSchema as AiConfigSchema } from "./generated/index.js" + +/** + * Options for {@link AI.finetune}. `trainingData` accepts either a built + * {@link Resource} (a Storage bucket) or its id string. + */ +export interface FinetuneOptions { + /** + * Provider-native base-model id to tune (an Amazon Nova id on Bedrock, a + * Gemini model on Vertex, or a gpt-4o family model on Foundry). + */ + baseModel: string + /** The Storage resource (or its id) holding the JSONL training dataset. */ + trainingData: Resource | string + /** Object key of the training file within `trainingData`. Defaults to `training.jsonl`. */ + trainingKey?: string + /** Public model id apps use to invoke the tuned model. Defaults to `-tuned`. */ + servedModelId?: string + /** The fine-tuning method. Defaults to `"sft"` (supervised fine-tuning). */ + method?: FinetuneMethod +} + +/** + * Represents an AI Gateway resource that provides a unified interface to + * managed AI inference services across cloud providers. + */ +export class AI { + private _config: Partial = {} + + /** + * Creates a new AI builder. + * @param id Identifier for the AI resource. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]). Maximum 64 characters. + */ + constructor(id: string) { + this._config.id = id + } + + /** + * Declares that this resource should fine-tune a base model in the customer's + * cloud before serving it. The tuning job reads the JSONL dataset from the + * given Storage bucket (S3 / GCS / Blob) under the workload's ambient + * identity — the data never leaves the customer's cloud — and the tuned model + * is served through the same gateway under `servedModelId` (default + * `-tuned`). Omit this call for a pure inference gateway. + * + * @param options Fine-tuning configuration. + * @returns This builder, for chaining. + */ + public finetune(options: FinetuneOptions): this { + const trainingData = + typeof options.trainingData === "string" + ? options.trainingData + : options.trainingData.config.id + + this._config.finetune = { + baseModel: options.baseModel, + trainingData, + ...(options.trainingKey !== undefined ? { trainingKey: options.trainingKey } : {}), + ...(options.servedModelId !== undefined ? { servedModelId: options.servedModelId } : {}), + ...(options.method !== undefined ? { method: options.method } : {}), + } + return this + } + + /** + * Returns a ResourceType representing any AI resource. + * Used for creating permission targets that apply to all AI resources. + * @returns The "ai" resource type. + */ + public static any(): ResourceType { + return "ai" + } + + /** + * Builds and validates the AI configuration. + * @returns An immutable Resource representing the configured AI Gateway. + * @throws Error if the AI configuration is invalid. + */ + public build(): Resource { + const config = AiSchema.parse(this._config) + + return new Resource({ + type: "ai", + ...config, + }) + } +} diff --git a/packages/core/src/common-errors.ts b/packages/core/src/common-errors.ts index 6eb3ccc58..8de4e7e1c 100644 --- a/packages/core/src/common-errors.ts +++ b/packages/core/src/common-errors.ts @@ -53,3 +53,33 @@ export const UnexpectedResourceTypeError = defineError({ internal: false, httpStatusCode: 400, }) + +/** + * Error thrown when a binding's projected configuration is invalid. + */ +export const InvalidBindingConfigError = defineError({ + code: "INVALID_BINDING_CONFIG", + context: z.object({ + message: z.string(), + suggestion: z.string().optional(), + }), + message: ({ message, suggestion }) => (suggestion ? `${message}. ${suggestion}` : message), + retryable: false, + internal: false, + httpStatusCode: 400, +}) + +/** + * Error thrown when a binding is not found. + */ +export const BindingNotFoundError = defineError({ + code: "BINDING_NOT_FOUND", + context: z.object({ + bindingName: z.string(), + bindingType: z.string(), + }), + message: ({ bindingName, bindingType }) => `${bindingType} binding '${bindingName}' not found`, + retryable: false, + internal: false, + httpStatusCode: 404, +}) diff --git a/packages/core/src/generated/index.ts b/packages/core/src/generated/index.ts index 8b856d006..d74b50a28 100644 --- a/packages/core/src/generated/index.ts +++ b/packages/core/src/generated/index.ts @@ -1,4 +1,8 @@ export type { AgentStatus } from "./zod/agent-status-schema.js"; +export type { AiHeartbeatData } from "./zod/ai-heartbeat-data-schema.js"; +export type { AiHeartbeatStatus } from "./zod/ai-heartbeat-status-schema.js"; +export type { AiOutputs } from "./zod/ai-outputs-schema.js"; +export type { Ai } from "./zod/ai-schema.js"; export type { AlienError } from "./zod/alien-error-schema.js"; export type { AlienEvent } from "./zod/alien-event-schema.js"; export type { Architecture } from "./zod/architecture-schema.js"; @@ -8,6 +12,7 @@ export type { ArtifactRegistryOutputs } from "./zod/artifact-registry-outputs-sc export type { ArtifactRegistry } from "./zod/artifact-registry-schema.js"; export type { AuroraPostgresHeartbeatData } from "./zod/aurora-postgres-heartbeat-data-schema.js"; export type { AwsArtifactRegistryImportData } from "./zod/aws-artifact-registry-import-data-schema.js"; +export type { AwsBedrockAiHeartbeatData } from "./zod/aws-bedrock-ai-heartbeat-data-schema.js"; export type { AwsBuildImportData } from "./zod/aws-build-import-data-schema.js"; export type { AwsCodeBuildHeartbeatData } from "./zod/aws-code-build-heartbeat-data-schema.js"; export type { AwsComputeClusterHeartbeatData } from "./zod/aws-compute-cluster-heartbeat-data-schema.js"; @@ -61,6 +66,7 @@ export type { AzureCustomCertificateConfig } from "./zod/azure-custom-certificat export type { AzureDaemonHeartbeatData } from "./zod/azure-daemon-heartbeat-data-schema.js"; export type { AzureFlexibleServerPostgresHeartbeatData } from "./zod/azure-flexible-server-postgres-heartbeat-data-schema.js"; export type { AzureFlexibleServerPostgresImportData } from "./zod/azure-flexible-server-postgres-import-data-schema.js"; +export type { AzureFoundryAiHeartbeatData } from "./zod/azure-foundry-ai-heartbeat-data-schema.js"; export type { AzureKeyVaultHeartbeatData } from "./zod/azure-key-vault-heartbeat-data-schema.js"; export type { AzureKvImportData } from "./zod/azure-kv-import-data-schema.js"; export type { AzureManagedIdentityServiceAccountHeartbeatData } from "./zod/azure-managed-identity-service-account-heartbeat-data-schema.js"; @@ -155,7 +161,10 @@ export type { Envelope } from "./zod/envelope-schema.js"; export type { EventChange } from "./zod/event-change-schema.js"; export type { EventState } from "./zod/event-state-schema.js"; export type { ExposeProtocol } from "./zod/expose-protocol-schema.js"; +export type { ExternalAiHeartbeatData } from "./zod/external-ai-heartbeat-data-schema.js"; export type { FailureDomainSelection } from "./zod/failure-domain-selection-schema.js"; +export type { FinetuneMethod } from "./zod/finetune-method-schema.js"; +export type { FinetuneSpec } from "./zod/finetune-spec-schema.js"; export type { GcpArtifactRegistryHeartbeatData } from "./zod/gcp-artifact-registry-heartbeat-data-schema.js"; export type { GcpArtifactRegistryImportData } from "./zod/gcp-artifact-registry-import-data-schema.js"; export type { GcpBuildImportData } from "./zod/gcp-build-import-data-schema.js"; @@ -185,6 +194,7 @@ export type { GcpServiceActivationImportData } from "./zod/gcp-service-activatio export type { GcpServiceUsageActivationHeartbeatData } from "./zod/gcp-service-usage-activation-heartbeat-data-schema.js"; export type { GcpStorageImportData } from "./zod/gcp-storage-import-data-schema.js"; export type { GcpVaultImportData } from "./zod/gcp-vault-import-data-schema.js"; +export type { GcpVertexAiHeartbeatData } from "./zod/gcp-vertex-ai-heartbeat-data-schema.js"; export type { GcpVpcNetworkHeartbeatData } from "./zod/gcp-vpc-network-heartbeat-data-schema.js"; export type { GcpWorkerImportData } from "./zod/gcp-worker-import-data-schema.js"; export type { GpuSpec } from "./zod/gpu-spec-schema.js"; @@ -371,6 +381,10 @@ export type { WorkerTrigger } from "./zod/worker-trigger-schema.js"; export type { WorkloadHeartbeatStatus } from "./zod/workload-heartbeat-status-schema.js"; export type { WorkloadReplicaStatus } from "./zod/workload-replica-status-schema.js"; export { AgentStatusSchema } from "./zod/agent-status-schema.js"; +export { AiHeartbeatDataSchema } from "./zod/ai-heartbeat-data-schema.js"; +export { AiHeartbeatStatusSchema } from "./zod/ai-heartbeat-status-schema.js"; +export { AiOutputsSchema } from "./zod/ai-outputs-schema.js"; +export { AiSchema } from "./zod/ai-schema.js"; export { AlienErrorSchema } from "./zod/alien-error-schema.js"; export { AlienEventSchema } from "./zod/alien-event-schema.js"; export { ArchitectureSchema } from "./zod/architecture-schema.js"; @@ -380,6 +394,7 @@ export { ArtifactRegistryOutputsSchema } from "./zod/artifact-registry-outputs-s export { ArtifactRegistrySchema } from "./zod/artifact-registry-schema.js"; export { AuroraPostgresHeartbeatDataSchema } from "./zod/aurora-postgres-heartbeat-data-schema.js"; export { AwsArtifactRegistryImportDataSchema } from "./zod/aws-artifact-registry-import-data-schema.js"; +export { AwsBedrockAiHeartbeatDataSchema } from "./zod/aws-bedrock-ai-heartbeat-data-schema.js"; export { AwsBuildImportDataSchema } from "./zod/aws-build-import-data-schema.js"; export { AwsCodeBuildHeartbeatDataSchema } from "./zod/aws-code-build-heartbeat-data-schema.js"; export { AwsComputeClusterHeartbeatDataSchema } from "./zod/aws-compute-cluster-heartbeat-data-schema.js"; @@ -433,6 +448,7 @@ export { AzureCustomCertificateConfigSchema } from "./zod/azure-custom-certifica export { AzureDaemonHeartbeatDataSchema } from "./zod/azure-daemon-heartbeat-data-schema.js"; export { AzureFlexibleServerPostgresHeartbeatDataSchema } from "./zod/azure-flexible-server-postgres-heartbeat-data-schema.js"; export { AzureFlexibleServerPostgresImportDataSchema } from "./zod/azure-flexible-server-postgres-import-data-schema.js"; +export { AzureFoundryAiHeartbeatDataSchema } from "./zod/azure-foundry-ai-heartbeat-data-schema.js"; export { AzureKeyVaultHeartbeatDataSchema } from "./zod/azure-key-vault-heartbeat-data-schema.js"; export { AzureKvImportDataSchema } from "./zod/azure-kv-import-data-schema.js"; export { AzureManagedIdentityServiceAccountHeartbeatDataSchema } from "./zod/azure-managed-identity-service-account-heartbeat-data-schema.js"; @@ -527,7 +543,10 @@ export { EnvelopeSchema } from "./zod/envelope-schema.js"; export { EventChangeSchema } from "./zod/event-change-schema.js"; export { EventStateSchema } from "./zod/event-state-schema.js"; export { ExposeProtocolSchema } from "./zod/expose-protocol-schema.js"; +export { ExternalAiHeartbeatDataSchema } from "./zod/external-ai-heartbeat-data-schema.js"; export { FailureDomainSelectionSchema } from "./zod/failure-domain-selection-schema.js"; +export { FinetuneMethodSchema } from "./zod/finetune-method-schema.js"; +export { FinetuneSpecSchema } from "./zod/finetune-spec-schema.js"; export { GcpArtifactRegistryHeartbeatDataSchema } from "./zod/gcp-artifact-registry-heartbeat-data-schema.js"; export { GcpArtifactRegistryImportDataSchema } from "./zod/gcp-artifact-registry-import-data-schema.js"; export { GcpBuildImportDataSchema } from "./zod/gcp-build-import-data-schema.js"; @@ -557,6 +576,7 @@ export { GcpServiceActivationImportDataSchema } from "./zod/gcp-service-activati export { GcpServiceUsageActivationHeartbeatDataSchema } from "./zod/gcp-service-usage-activation-heartbeat-data-schema.js"; export { GcpStorageImportDataSchema } from "./zod/gcp-storage-import-data-schema.js"; export { GcpVaultImportDataSchema } from "./zod/gcp-vault-import-data-schema.js"; +export { GcpVertexAiHeartbeatDataSchema } from "./zod/gcp-vertex-ai-heartbeat-data-schema.js"; export { GcpVpcNetworkHeartbeatDataSchema } from "./zod/gcp-vpc-network-heartbeat-data-schema.js"; export { GcpWorkerImportDataSchema } from "./zod/gcp-worker-import-data-schema.js"; export { GpuSpecSchema } from "./zod/gpu-spec-schema.js"; diff --git a/packages/core/src/generated/schemas/ai.json b/packages/core/src/generated/schemas/ai.json new file mode 100644 index 000000000..33a143f78 --- /dev/null +++ b/packages/core/src/generated/schemas/ai.json @@ -0,0 +1 @@ +{"type":"object","description":"Represents an AI Gateway resource that provides a unified interface to\nmanaged AI inference services across cloud providers.\n\nBYO-key external providers (OpenAI/Anthropic) are NOT declared here. Like any\nother BYO infrastructure (e.g. external Redis for `kv`), an external AI\nprovider is supplied at deploy time as an `ExternalBinding::Ai` in the\nstack's external-bindings map; the executor then skips the cloud controller.\n\nWhen `finetune` is set, the resource also tunes a base model in the\ncustomer's cloud and serves the result through the same gateway (see\n[`FinetuneSpec`]).","required":["id"],"properties":{"finetune":{"oneOf":[{"type":"null"},{"description":"Optional fine-tuning declaration. When present, the resource tunes\n`finetune.base_model` on the customer's cloud and serves the result\nalongside the base models. Absent for a pure inference gateway.","type":"object","required":["baseModel","trainingData"],"properties":{"baseModel":{"type":"string","description":"Provider-native base-model identifier to tune (e.g. an Amazon Nova model\nid on Bedrock, a Gemini model on Vertex, or a gpt-4o family model on\nFoundry). Validated against the target cloud when a runtime job is submitted."},"method":{"description":"The fine-tuning method. Defaults to supervised fine-tuning.","type":"string","enum":["sft","dpo","lora"],"x-readme-ref-name":"FinetuneMethod"},"servedModelId":{"type":["string","null"],"description":"The public model id apps use to invoke the tuned model through the\ngateway (the `model` field in an OpenAI-compatible request). Defaults to\n`-tuned`."},"trainingData":{"type":"string","description":"The storage resource holding the JSONL training dataset. The gateway\nreads it from the customer bucket the storage resolves to; the data\nnever leaves the customer's cloud."},"trainingKey":{"type":"string","description":"Object key of the training file within `training_data`.\nDefaults to `training.jsonl`."}},"additionalProperties":false,"x-readme-ref-name":"FinetuneSpec"}]},"id":{"type":"string","description":"Identifier for the AI resource. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]).\nMaximum 64 characters."}},"additionalProperties":false,"x-readme-ref-name":"Ai"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/aiHeartbeatData.json b/packages/core/src/generated/schemas/aiHeartbeatData.json new file mode 100644 index 000000000..11a7a0d55 --- /dev/null +++ b/packages/core/src/generated/schemas/aiHeartbeatData.json @@ -0,0 +1 @@ +{"oneOf":[{"allOf":[{"type":"object","required":["status","region"],"properties":{"region":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AwsBedrockAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsBedrock"]}}}]},{"allOf":[{"type":"object","required":["status","project","location"],"properties":{"location":{"type":"string"},"project":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"GcpVertexAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpVertex"]}}}]},{"allOf":[{"type":"object","required":["status","accountName"],"properties":{"accountName":{"type":"string"},"endpoint":{"type":["string","null"]},"location":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AzureFoundryAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureFoundry"]}}}]},{"allOf":[{"type":"object","required":["status","provider"],"properties":{"provider":{"type":"string","description":"The BYO-key provider serving this binding (e.g. \"openai\"). Used on the Local\nplatform, where the app brings its own provider key instead of an ambient cloud."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"ExternalAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["external"]}}}]}],"x-readme-ref-name":"AiHeartbeatData"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/aiHeartbeatStatus.json b/packages/core/src/generated/schemas/aiHeartbeatStatus.json new file mode 100644 index 000000000..7c759fbb7 --- /dev/null +++ b/packages/core/src/generated/schemas/aiHeartbeatStatus.json @@ -0,0 +1 @@ +{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/aiOutputs.json b/packages/core/src/generated/schemas/aiOutputs.json new file mode 100644 index 000000000..67da37925 --- /dev/null +++ b/packages/core/src/generated/schemas/aiOutputs.json @@ -0,0 +1 @@ +{"type":"object","description":"Outputs generated by a successfully provisioned AI Gateway resource.","required":["provider"],"properties":{"account":{"type":["string","null"],"description":"The provider account or project identifier, if applicable."},"endpoint":{"type":["string","null"],"description":"The provider endpoint URL, if applicable."},"provider":{"type":"string","description":"The AI provider name (e.g., \"bedrock\", \"vertex\", \"foundry\", \"external\")."}},"x-readme-ref-name":"AiOutputs"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/awsBedrockAiHeartbeatData.json b/packages/core/src/generated/schemas/awsBedrockAiHeartbeatData.json new file mode 100644 index 000000000..dc421f530 --- /dev/null +++ b/packages/core/src/generated/schemas/awsBedrockAiHeartbeatData.json @@ -0,0 +1 @@ +{"type":"object","required":["status","region"],"properties":{"region":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AwsBedrockAiHeartbeatData"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/azureFoundryAiHeartbeatData.json b/packages/core/src/generated/schemas/azureFoundryAiHeartbeatData.json new file mode 100644 index 000000000..2607ba6c3 --- /dev/null +++ b/packages/core/src/generated/schemas/azureFoundryAiHeartbeatData.json @@ -0,0 +1 @@ +{"type":"object","required":["status","accountName"],"properties":{"accountName":{"type":"string"},"endpoint":{"type":["string","null"]},"location":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AzureFoundryAiHeartbeatData"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/externalAiHeartbeatData.json b/packages/core/src/generated/schemas/externalAiHeartbeatData.json new file mode 100644 index 000000000..a06d83cb9 --- /dev/null +++ b/packages/core/src/generated/schemas/externalAiHeartbeatData.json @@ -0,0 +1 @@ +{"type":"object","required":["status","provider"],"properties":{"provider":{"type":"string","description":"The BYO-key provider serving this binding (e.g. \"openai\"). Used on the Local\nplatform, where the app brings its own provider key instead of an ambient cloud."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"ExternalAiHeartbeatData"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/finetuneMethod.json b/packages/core/src/generated/schemas/finetuneMethod.json new file mode 100644 index 000000000..afaa49566 --- /dev/null +++ b/packages/core/src/generated/schemas/finetuneMethod.json @@ -0,0 +1 @@ +{"type":"string","description":"The fine-tuning method applied to the base model.\n\nThe gateway-side controllers map each variant onto the provider's native\ntechnique: `Sft` is supervised fine-tuning (all three clouds), `Dpo` is\ndirect preference optimization (Bedrock/Foundry), and `Lora` requests a\nparameter-efficient adapter where the provider exposes it. Providers that\nonly implement a subset reject unsupported methods at job-submit time.","enum":["sft","dpo","lora"],"x-readme-ref-name":"FinetuneMethod"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/finetuneSpec.json b/packages/core/src/generated/schemas/finetuneSpec.json new file mode 100644 index 000000000..eef867b2d --- /dev/null +++ b/packages/core/src/generated/schemas/finetuneSpec.json @@ -0,0 +1 @@ +{"type":"object","description":"Declares that an [`Ai`] resource should fine-tune a base model in the\ncustomer's cloud before serving it.\n\nThis is a *capability declaration*, not a deploy-time trigger: a resource\nwith a `finetune` spec provisions and is Ready immediately (no job runs at\ndeploy). The declaration flows to the gateway as a fine-tuning capability;\nthe app then starts a job at runtime by calling `ai(\"\").finetune(...)`,\nwhich the gateway submits to the provider (Bedrock\n`CreateModelCustomizationJob`, Vertex tuning job, or Foundry\n`fine_tuning.jobs`) under the workload's ambient identity, reading the\ntraining data from the customer-owned [`Storage`](crate::Storage) bucket\n(S3 / GCS / Blob) referenced by `training_data`. Once a job completes, the\ngateway serves the tuned model under `served_model_id`, rediscovering it by\nconvention. Base-model inference is unaffected — an `Ai` without a\n`finetune` spec behaves exactly as before.","required":["baseModel","trainingData"],"properties":{"baseModel":{"type":"string","description":"Provider-native base-model identifier to tune (e.g. an Amazon Nova model\nid on Bedrock, a Gemini model on Vertex, or a gpt-4o family model on\nFoundry). Validated against the target cloud when a runtime job is submitted."},"method":{"description":"The fine-tuning method. Defaults to supervised fine-tuning.","type":"string","enum":["sft","dpo","lora"],"x-readme-ref-name":"FinetuneMethod"},"servedModelId":{"type":["string","null"],"description":"The public model id apps use to invoke the tuned model through the\ngateway (the `model` field in an OpenAI-compatible request). Defaults to\n`-tuned`."},"trainingData":{"type":"string","description":"The storage resource holding the JSONL training dataset. The gateway\nreads it from the customer bucket the storage resolves to; the data\nnever leaves the customer's cloud."},"trainingKey":{"type":"string","description":"Object key of the training file within `training_data`.\nDefaults to `training.jsonl`."}},"additionalProperties":false,"x-readme-ref-name":"FinetuneSpec"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/gcpVertexAiHeartbeatData.json b/packages/core/src/generated/schemas/gcpVertexAiHeartbeatData.json new file mode 100644 index 000000000..c06e05e8e --- /dev/null +++ b/packages/core/src/generated/schemas/gcpVertexAiHeartbeatData.json @@ -0,0 +1 @@ +{"type":"object","required":["status","project","location"],"properties":{"location":{"type":"string"},"project":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"GcpVertexAiHeartbeatData"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/resourceHeartbeat.json b/packages/core/src/generated/schemas/resourceHeartbeat.json index a83989e5d..f04b1a554 100644 --- a/packages/core/src/generated/schemas/resourceHeartbeat.json +++ b/packages/core/src/generated/schemas/resourceHeartbeat.json @@ -1 +1 @@ -{"type":"object","required":["resourceId","resourceType","controllerPlatform","backend","observedAt","data","raw"],"properties":{"backend":{"type":"string","enum":["aws","gcp","azure","kubernetes","local","managed","external","test"],"x-readme-ref-name":"HeartbeatBackend"},"controllerPlatform":{"type":"string","description":"Represents the target cloud platform.","enum":["aws","gcp","azure","kubernetes","machines","local","test"],"x-readme-ref-name":"Platform"},"data":{"oneOf":[{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent","publicAccessBlockPresent"],"properties":{"blockPublicAcls":{"type":["boolean","null"]},"blockPublicPolicy":{"type":["boolean","null"]},"bucketAclPresent":{"type":["boolean","null"]},"bucketLocation":{"type":["string","null"]},"bucketPolicyPresent":{"type":["boolean","null"]},"encryptionConfigPresent":{"type":"boolean"},"encryptionEnabled":{"type":["boolean","null"]},"ignorePublicAcls":{"type":["boolean","null"]},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"publicAccessBlockPresent":{"type":"boolean"},"region":{"type":["string","null"]},"restrictPublicBuckets":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"versioningEnabled":{"type":["boolean","null"]},"versioningStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsS3StorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsS3"]}}}]},{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent"],"properties":{"bucketId":{"type":["string","null"]},"defaultKmsKeyName":{"type":["string","null"]},"encryptionConfigPresent":{"type":"boolean"},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"location":{"type":["string","null"]},"locationType":{"type":["string","null"]},"name":{"type":"string"},"publicAccessPrevention":{"type":["string","null"]},"retentionPeriod":{"type":["string","null"]},"retentionPolicyEffectiveTime":{"type":["string","null"]},"retentionPolicyIsLocked":{"type":["boolean","null"]},"softDeleteEffectiveTime":{"type":["string","null"]},"softDeleteRetentionDurationSeconds":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"storageClass":{"type":["string","null"]},"uniformBucketLevelAccessEnabled":{"type":["boolean","null"]},"uniformBucketLevelAccessLockedTime":{"type":["string","null"]},"versioningEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"GcpCloudStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudStorage"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"accessTier":{"type":["string","null"]},"accountKind":{"type":["string","null"]},"allowBlobPublicAccess":{"type":["boolean","null"]},"blobDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"blobDeleteRetentionEnabled":{"type":["boolean","null"]},"blobEncryptionEnabled":{"type":["boolean","null"]},"blobVersioningEnabled":{"type":["boolean","null"]},"changeFeedEnabled":{"type":["boolean","null"]},"changeFeedRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionEnabled":{"type":["boolean","null"]},"containerPublicAccess":{"type":["string","null"]},"encryptionKeySource":{"type":["string","null"]},"fileEncryptionEnabled":{"type":["boolean","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"primaryLocation":{"type":["string","null"]},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"queueEncryptionEnabled":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"secondaryLocation":{"type":["string","null"]},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"statusOfPrimary":{"type":["string","null"]},"statusOfSecondary":{"type":["string","null"]},"storageAccountName":{"type":["string","null"]},"tableEncryptionEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureBlobStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureBlob"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"}},"x-readme-ref-name":"LocalStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"StorageHeartbeatData"},"resourceType":{"type":"string","enum":["storage"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","functionName","layerCount","functionUrlCorsPresent","triggerCount"],"properties":{"codeSha256":{"type":["string","null"]},"functionName":{"type":"string"},"functionUrlAuthType":{"type":["string","null"]},"functionUrlCorsPresent":{"type":"boolean"},"lastModified":{"type":["string","null"]},"lastUpdateStatus":{"type":["string","null"]},"lastUpdateStatusReason":{"type":["string","null"]},"lastUpdateStatusReasonCode":{"type":["string","null"]},"layerCount":{"type":"integer","format":"int32","minimum":0},"memorySizeMb":{"type":["integer","null"],"format":"int64"},"packageType":{"type":["string","null"]},"revisionId":{"type":["string","null"]},"runtime":{"type":["string","null"]},"state":{"type":["string","null"]},"stateReason":{"type":["string","null"]},"stateReasonCode":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"timeoutSeconds":{"type":["integer","null"],"format":"int64"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"version":{"type":["string","null"]}},"x-readme-ref-name":"AwsLambdaWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsLambda"]}}}]},{"allOf":[{"type":"object","required":["status","service","urls","trafficCount"],"properties":{"containerImage":{"type":["string","null"]},"cpuLimit":{"type":["string","null"]},"generation":{"type":["integer","null"],"format":"int64"},"latestCreatedRevision":{"type":["string","null"]},"latestReadyRevision":{"type":["string","null"]},"maxInstanceCount":{"type":["integer","null"],"format":"int32"},"memoryLimit":{"type":["string","null"]},"minInstanceCount":{"type":["integer","null"],"format":"int32"},"observedGeneration":{"type":["integer","null"],"format":"int64"},"region":{"type":["string","null"]},"service":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"trafficCount":{"type":"integer","format":"int32","minimum":0},"uri":{"type":["string","null"]},"urls":{"type":"array","items":{"type":"string"}}},"x-readme-ref-name":"GcpCloudRunWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudRun"]}}}]},{"allOf":[{"type":"object","required":["status","appName"],"properties":{"appName":{"type":"string"},"cpu":{"type":["number","null"],"format":"double"},"environmentName":{"type":["string","null"]},"ingressFqdn":{"type":["string","null"]},"maxReplicas":{"type":["integer","null"],"format":"int32"},"memory":{"type":["string","null"]},"minReplicas":{"type":["integer","null"],"format":"int32"},"provisioningState":{"type":["string","null"]},"revision":{"type":["string","null"]},"runningStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","triggerCount","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","commandSupported","imagePathPresent","triggerCount","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"imagePathPresent":{"type":"boolean"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"process":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"readinessProbeOk":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"LocalWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"WorkerHeartbeatData"},"resourceType":{"type":"string","enum":["worker"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","containerId","schedulingMode","replicas","attentionCount","replicaUnits","events"],"properties":{"attentionCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":"string"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"latestUpdateTimestamp":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"observedImage":{"type":["string","null"]},"replicaUnits":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"schedulingMode":{"type":"string","enum":["replicated","stateful","daemon"],"x-readme-ref-name":"HorizonWorkloadSchedulingMode"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"HorizonContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["horizonPlatform"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","portCount","bindMountCount","runtimeReachable","events"],"properties":{"bindMountCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":["string","null"]},"containerUnit":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"localUrl":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":["string","null"]},"portCount":{"type":"integer","format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeReachable":{"type":"boolean"},"runtimeStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ContainerHeartbeatData"},"resourceType":{"type":"string","enum":["container"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"GcpDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AzureDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"MachinesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","replicas","commandSupported","pods","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]}},"x-readme-ref-name":"KubernetesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","runtimeId","commandSupported","imagePathPresent","events"],"properties":{"commandSupported":{"type":"boolean"},"daemonInstance":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"daemonName":{"type":"string"},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"exitReason":{"type":["string","null"]},"imagePathPresent":{"type":"boolean"},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeId":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"DaemonHeartbeatData"},"resourceType":{"type":"string","enum":["daemon"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AwsComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"GcpComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AzureComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","machines"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"machines":{"type":"array","items":{"type":"object","required":["machineId","status","capacityGroup","zone","lastHeartbeat","replicaCount","drainForce"],"properties":{"capacityGroup":{"type":"string"},"cpuCores":{"type":["number","null"],"format":"double"},"drainBlockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainForce":{"type":"boolean"},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"horizondVersion":{"type":["string","null"]},"lastHeartbeat":{"type":"string"},"machineId":{"type":"string"},"memoryBytes":{"type":["integer","null"],"format":"int64"},"overlayIp":{"type":["string","null"]},"publicIp":{"type":["string","null"]},"replicaCount":{"type":"integer","format":"int64"},"status":{"type":"string"},"zone":{"type":"string"}},"x-readme-ref-name":"MachinesComputeMachineStatus"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"MachinesComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","dockerAvailable","networkAvailable"],"properties":{"dockerApiVersion":{"type":["string","null"]},"dockerArch":{"type":["string","null"]},"dockerAvailable":{"type":"boolean"},"dockerOs":{"type":["string","null"]},"dockerVersion":{"type":["string","null"]},"hostIdentifier":{"type":["string","null"]},"name":{"type":"string"},"networkAvailable":{"type":"boolean"},"networkName":{"type":["string","null"]},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"runningContainers":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"},"trackedContainers":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"LocalComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ComputeClusterHeartbeatData"},"resourceType":{"type":"string","enum":["compute-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","nodeCounts","podCounts","name","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":["string","null"]},"nodeCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"nodeStatuses":{"type":"array","items":{"type":"object","required":["name","ready","roles","labels","allocatable","capacity"],"properties":{"allocatable":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"capacity":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesNodeConditionStatus"}},"containerRuntimeVersion":{"type":["string","null"]},"kubeletVersion":{"type":["string","null"]},"labels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"ready":{"type":"boolean"},"roles":{"type":"array","items":{"type":"string"}},"uid":{"type":["string","null"]},"usage":{"oneOf":[{"type":"null"},{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"KubernetesNodeUsage"}]}},"x-readme-ref-name":"KubernetesClusterNodeStatus"}},"podCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesClusterHeartbeatData"},"resourceType":{"type":"string","enum":["kubernetes-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","approximateCounts"],"properties":{"approximateCounts":{"type":"boolean"},"approximateDelayedMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateInFlightMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateVisibleMessages":{"type":["integer","null"],"format":"int64","minimum":0},"contentBasedDeduplication":{"type":["boolean","null"]},"deduplicationScope":{"type":["string","null"]},"delaySeconds":{"type":["integer","null"],"format":"int32","minimum":0},"fifoQueue":{"type":["boolean","null"]},"fifoThroughputLimit":{"type":["string","null"]},"kmsDataKeyReusePeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"kmsMasterKeyId":{"type":["string","null"]},"maximumMessageSize":{"type":["integer","null"],"format":"int32","minimum":0},"messageRetentionPeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"queueArn":{"type":["string","null"]},"queueUrl":{"type":["string","null"]},"receiveMessageWaitTimeSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"redriveAllowPolicy":{"type":["string","null"]},"redrivePolicy":{"type":["string","null"]},"region":{"type":["string","null"]},"sqsManagedSseEnabled":{"type":["boolean","null"]},"sseEnabled":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"visibilityTimeoutSeconds":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"AwsSqsQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsSqs"]}}}]},{"allOf":[{"type":"object","required":["status","topicName","topicLabels","subscriptionLabels","messageStorageAllowedPersistenceRegions","subscriptionPushAttributes"],"properties":{"endpoint":{"type":["string","null"]},"kmsKeyName":{"type":["string","null"]},"messageStorageAllowedPersistenceRegions":{"type":"array","items":{"type":"string"}},"messageStorageEnforceInTransit":{"type":["boolean","null"]},"projectId":{"type":["string","null"]},"schemaEncoding":{"type":["string","null"]},"schemaFirstRevisionId":{"type":["string","null"]},"schemaLastRevisionId":{"type":["string","null"]},"schemaName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"subscriptionAckDeadlineSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterMaxDeliveryAttempts":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterTopic":{"type":["string","null"]},"subscriptionDetached":{"type":["boolean","null"]},"subscriptionEnableMessageOrdering":{"type":["boolean","null"]},"subscriptionFilter":{"type":["string","null"]},"subscriptionFullName":{"type":["string","null"]},"subscriptionLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionMessageRetentionDuration":{"type":["string","null"]},"subscriptionName":{"type":["string","null"]},"subscriptionPushAttributes":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionPushConfigPresent":{"type":["boolean","null"]},"subscriptionPushEndpoint":{"type":["string","null"]},"subscriptionPushNoWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionPushOidcAudience":{"type":["string","null"]},"subscriptionPushOidcServiceAccountEmail":{"type":["string","null"]},"subscriptionPushPubsubWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionRetainAckedMessages":{"type":["boolean","null"]},"subscriptionState":{"type":["string","null"]},"topicFullName":{"type":["string","null"]},"topicLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"topicMessageRetentionDuration":{"type":["string","null"]},"topicName":{"type":"string"},"topicState":{"type":["string","null"]}},"x-readme-ref-name":"GcpPubSubQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpPubSub"]}}}]},{"allOf":[{"type":"object","required":["status","name","namespaceName"],"properties":{"accessedAt":{"type":["string","null"]},"activeMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"autoDeleteOnIdle":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"deadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"deadLetteringOnMessageExpiration":{"type":["boolean","null"]},"defaultMessageTimeToLive":{"type":["string","null"]},"duplicateDetectionHistoryTimeWindow":{"type":["string","null"]},"enableBatchedOperations":{"type":["boolean","null"]},"enableExpress":{"type":["boolean","null"]},"enablePartitioning":{"type":["boolean","null"]},"endpoint":{"type":["string","null"]},"forwardDeadLetteredMessagesTo":{"type":["string","null"]},"forwardTo":{"type":["string","null"]},"lockDuration":{"type":["string","null"]},"maxDeliveryCount":{"type":["integer","null"],"format":"int32","minimum":0},"maxMessageSizeInKilobytes":{"type":["integer","null"],"format":"int64","minimum":0},"maxSizeInMegabytes":{"type":["integer","null"],"format":"int32","minimum":0},"messageCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"namespaceName":{"type":"string"},"queueStatus":{"type":["string","null"]},"requiresDuplicateDetection":{"type":["boolean","null"]},"requiresSession":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"scheduledMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"sizeInBytes":{"type":["integer","null"],"format":"int64","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"transferDeadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"transferMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"updatedAt":{"type":["string","null"]}},"x-readme-ref-name":"AzureServiceBusQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureServiceBus"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"name":{"type":"string"},"path":{"type":["string","null"]},"serviceStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"}},"x-readme-ref-name":"LocalQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"QueueHeartbeatData"},"resourceType":{"type":"string","enum":["queue"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","keySchema"],"properties":{"billingMode":{"type":["string","null"]},"deletionProtectionEnabled":{"type":["boolean","null"]},"globalSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"itemCount":{"type":["integer","null"],"format":"int64","minimum":0},"keySchema":{"type":"array","items":{"type":"object","required":["attributeName","keyType"],"properties":{"attributeName":{"type":"string"},"keyType":{"type":"string"}},"x-readme-ref-name":"AwsDynamoDbKeySchemaElement"}},"localSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"region":{"type":["string","null"]},"replicaCount":{"type":["integer","null"],"format":"int32","minimum":0},"restoreInProgress":{"type":["boolean","null"]},"sseStatus":{"type":["string","null"]},"sseType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"streamEnabled":{"type":["boolean","null"]},"streamViewType":{"type":["string","null"]},"tableArn":{"type":["string","null"]},"tableClass":{"type":["string","null"]},"tableSizeBytes":{"type":["integer","null"],"format":"int64","minimum":0},"tableStatus":{"type":["string","null"]},"ttlAttributeName":{"type":["string","null"]},"ttlStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsDynamoDbKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsDynamoDb"]}}}]},{"allOf":[{"type":"object","required":["status","databaseName","cmekEnabled","sourceInfoPresent"],"properties":{"appEngineIntegrationMode":{"type":["string","null"]},"cmekEnabled":{"type":"boolean"},"concurrencyMode":{"type":["string","null"]},"createTime":{"type":["string","null"]},"databaseEdition":{"type":["string","null"]},"databaseName":{"type":"string"},"databaseType":{"type":["string","null"]},"deleteProtectionState":{"type":["string","null"]},"deleteTime":{"type":["string","null"]},"earliestVersionTime":{"type":["string","null"]},"endpoint":{"type":["string","null"]},"locationId":{"type":["string","null"]},"pointInTimeRecoveryEnablement":{"type":["string","null"]},"projectId":{"type":["string","null"]},"sourceInfoPresent":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"updateTime":{"type":["string","null"]},"versionRetentionPeriod":{"type":["string","null"]}},"x-readme-ref-name":"GcpFirestoreKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpFirestore"]}}}]},{"allOf":[{"type":"object","required":["status","tableName","storageAccountName","tableExists"],"properties":{"endpoint":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"signedIdentifierCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"storageAccountKind":{"type":["string","null"]},"storageAccountLocation":{"type":["string","null"]},"storageAccountName":{"type":"string"},"storageAccountPrimaryStatus":{"type":["string","null"]},"storageAccountProvisioningState":{"type":["string","null"]},"storageAccountResourceId":{"type":["string","null"]},"tableExists":{"type":"boolean"},"tableName":{"type":"string"}},"x-readme-ref-name":"AzureTableKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureTable"]}}}]},{"allOf":[{"type":"object","required":["status","name","path","pathExists","cloudMetadataSupported"],"properties":{"cloudMetadataSupported":{"type":"boolean"},"isDirectory":{"type":["boolean","null"]},"name":{"type":"string"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"}},"x-readme-ref-name":"LocalKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"KvHeartbeatData"},"resourceType":{"type":"string","enum":["kv"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"description":"AWS Aurora Serverless v2 backend.","type":"object","required":["status","clusterIdentifier","neverPauses"],"properties":{"clusterIdentifier":{"type":"string"},"endpoint":{"type":["string","null"]},"engineVersion":{"type":["string","null"]},"neverPauses":{"type":"boolean","description":"True when a `minCapacity: 0` instance has not reached 0 ACU over the observation\nwindow — it is silently paying always-on prices (auto-pause verification)."},"serverlessCapacity":{"type":["number","null"],"format":"double","description":"Latest sampled `ServerlessDatabaseCapacity` (ACU)."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"AuroraPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aurora"]}}}],"description":"AWS Aurora Serverless v2 backend."},{"allOf":[{"description":"GCP Cloud SQL backend.","type":"object","required":["status","instanceName"],"properties":{"databaseVersion":{"type":["string","null"]},"instanceName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudSqlPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["cloudSql"]}}}],"description":"GCP Cloud SQL backend."},{"allOf":[{"description":"Azure Flexible Server backend.","type":"object","required":["status","serverName"],"properties":{"serverName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"AzureFlexibleServerPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["flexibleServer"]}}}],"description":"Azure Flexible Server backend."},{"allOf":[{"description":"Local embedded Postgres backend.","type":"object","required":["status","name","version","processRunning"],"properties":{"name":{"type":"string"},"port":{"type":["integer","null"],"format":"int32","minimum":0},"processRunning":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":"string"}},"x-readme-ref-name":"LocalPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}],"description":"Local embedded Postgres backend."}],"x-readme-ref-name":"PostgresHeartbeatData"},"resourceType":{"type":"string","enum":["postgres"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","accountId","region","prefix","parameterMetadataSampled"],"properties":{"accountId":{"type":"string"},"hasMoreParameters":{"type":["boolean","null"]},"latestModifiedAt":{"type":["string","null"],"format":"date-time"},"parameterMetadataSampled":{"type":"boolean"},"prefix":{"type":"string"},"region":{"type":"string"},"sampledAdvancedTierCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledKmsKeyMetadataPresentCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledParameterCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledSecureStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringListCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"AwsParameterStoreVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsParameterStore"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","prefix","secretMetadataListed"],"properties":{"location":{"type":"string"},"prefix":{"type":"string"},"projectId":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"GcpSecretManagerVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpSecretManager"]}}}]},{"allOf":[{"type":"object","required":["status","name","softDeleteEnabled","softDeleteRetentionDays","rbacAuthorizationEnabled","publicNetworkAccess","accessPolicyCount","privateEndpointConnectionCount","secretMetadataListed"],"properties":{"accessPolicyCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":["string","null"]},"name":{"type":"string"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"purgeProtectionEnabled":{"type":["boolean","null"]},"rbacAuthorizationEnabled":{"type":"boolean"},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secretMetadataListed":{"type":"boolean"},"skuFamily":{"type":["string","null"]},"skuName":{"type":["string","null"]},"softDeleteEnabled":{"type":"boolean"},"softDeleteRetentionDays":{"type":"integer","format":"int32"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"},"vaultUri":{"type":["string","null"]}},"x-readme-ref-name":"AzureKeyVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureKeyVault"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","prefix","secretMetadataListed"],"properties":{"namespace":{"type":"string"},"prefix":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"KubernetesSecretVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesSecret"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists","secretMetadataListed"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"LocalVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"VaultHeartbeatData"},"resourceType":{"type":"string","enum":["vault"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","roleName","roleArn","roleId","path","createDate","assumeRolePolicyPresent","tagCount","managedTagCount","attachedPolicyCount","attachedPolicyNames","inlinePolicyCount","inlinePolicyNames","stackPermissionsApplied"],"properties":{"assumeRolePolicyPresent":{"type":"boolean"},"attachedPolicyCount":{"type":"integer","format":"int32","minimum":0},"attachedPolicyNames":{"type":"array","items":{"type":"string"}},"createDate":{"type":"string"},"description":{"type":["string","null"]},"inlinePolicyCount":{"type":"integer","format":"int32","minimum":0},"inlinePolicyNames":{"type":"array","items":{"type":"string"}},"lastUsedDate":{"type":["string","null"]},"lastUsedRegion":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"maxSessionDuration":{"type":["integer","null"],"format":"int32"},"path":{"type":"string"},"permissionsBoundaryArn":{"type":["string","null"]},"permissionsBoundaryType":{"type":["string","null"]},"roleArn":{"type":"string"},"roleId":{"type":"string"},"roleName":{"type":"string"},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tagCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsIamRoleServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","email","projectBindingCount","projectRoles","serviceAccountBindingCount","serviceAccountRoles"],"properties":{"description":{"type":["string","null"]},"disabled":{"type":["boolean","null"]},"displayName":{"type":["string","null"]},"email":{"type":"string"},"etag":{"type":["string","null"]},"name":{"type":["string","null"]},"oauth2ClientId":{"type":["string","null"]},"projectBindingCount":{"type":"integer","format":"int32","minimum":0},"projectId":{"type":["string","null"]},"projectRoles":{"type":"array","items":{"type":"string"}},"serviceAccountBindingCount":{"type":"integer","format":"int32","minimum":0},"serviceAccountRoles":{"type":"array","items":{"type":"string"}},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"uniqueId":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceId","resourceGroup","location","managedTagCount","roleAssignmentCount","roleAssignmentIds","customRoleDefinitionCount","customRoleDefinitionIds","stackPermissionsApplied"],"properties":{"clientId":{"type":["string","null"]},"customRoleDefinitionCount":{"type":"integer","format":"int32","minimum":0},"customRoleDefinitionIds":{"type":"array","items":{"type":"string"}},"isolationScope":{"type":["string","null"]},"location":{"type":"string"},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"principalId":{"type":["string","null"]},"resourceGroup":{"type":"string"},"resourceId":{"type":"string"},"roleAssignmentCount":{"type":"integer","format":"int32","minimum":0},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tenantId":{"type":["string","null"]},"type":{"type":["string","null"]}},"x-readme-ref-name":"AzureManagedIdentityServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]},{"allOf":[{"type":"object","required":["status","identity","configured"],"properties":{"configured":{"type":"boolean"},"identity":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"}},"x-readme-ref-name":"LocalServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ServiceAccountHeartbeatData"},"resourceType":{"type":"string","enum":["service-account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","publicSubnetIds","privateSubnetIds","availabilityZones","routeTableCount","isByoVpc"],"properties":{"availabilityZones":{"type":"array","items":{"type":"string"}},"cidrBlock":{"type":["string","null"]},"internetGatewayId":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"natGatewayId":{"type":["string","null"]},"privateSubnetIds":{"type":"array","items":{"type":"string"}},"publicSubnetIds":{"type":"array","items":{"type":"string"}},"routeTableCount":{"type":"integer","format":"int32","minimum":0},"securityGroupId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vpcId":{"type":["string","null"]},"vpcState":{"type":["string","null"]}},"x-readme-ref-name":"AwsVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVpc"],"properties":{"cidrBlock":{"type":["string","null"]},"cloudNatName":{"type":["string","null"]},"firewallName":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"networkName":{"type":["string","null"]},"networkSelfLink":{"type":["string","null"]},"region":{"type":["string","null"]},"routerName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"subnetworkName":{"type":["string","null"]},"subnetworkSelfLink":{"type":["string","null"]}},"x-readme-ref-name":"GcpVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVnet"],"properties":{"applicationGatewaySubnetName":{"type":["string","null"]},"cidrBlock":{"type":["string","null"]},"isByoVnet":{"type":"boolean"},"lastByoVnetVerificationErrorCode":{"type":["string","null"]},"location":{"type":["string","null"]},"natGatewayId":{"type":["string","null"]},"nsgId":{"type":["string","null"]},"privateEndpointSubnetName":{"type":["string","null"]},"privateSubnetName":{"type":["string","null"]},"publicIpId":{"type":["string","null"]},"publicSubnetName":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vnetName":{"type":["string","null"]},"vnetResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureVnetNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureVnet"]}}}]}],"x-readme-ref-name":"NetworkHeartbeatData"},"resourceType":{"type":"string","enum":["network"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","managementPermissionsApplied"],"properties":{"managementPermissionsApplied":{"type":"boolean"},"roleArn":{"type":["string","null"]},"roleName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"AwsRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","roleBound","impersonationGranted"],"properties":{"impersonationGranted":{"type":"boolean"},"roleBound":{"type":"boolean"},"serviceAccountEmail":{"type":["string","null"]},"serviceAccountUniqueId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"GcpRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","roleAssignmentIds"],"properties":{"ficName":{"type":["string","null"]},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"roleDefinitionId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"},"tenantId":{"type":["string","null"]},"uamiClientId":{"type":["string","null"]},"uamiPrincipalId":{"type":["string","null"]},"uamiResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]}],"x-readme-ref-name":"RemoteStackManagementHeartbeatData"},"resourceType":{"type":"string","enum":["remote-stack-management"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","registryId","region","registryUri","repositoryPrefix","repositoryCount","repositoriesTruncated","repositories"],"properties":{"pullRoleArn":{"type":["string","null"]},"pushRoleArn":{"type":["string","null"]},"region":{"type":"string"},"registryId":{"type":"string"},"registryUri":{"type":"string"},"repositories":{"type":"array","items":{"type":"object","required":["repositoryArn","registryId","repositoryName","repositoryUri","createdAt","kmsKeyPresent"],"properties":{"createdAt":{"type":"number","format":"double"},"encryptionType":{"type":["string","null"]},"imageTagMutability":{"type":["string","null"]},"kmsKeyPresent":{"type":"boolean"},"registryId":{"type":"string"},"repositoryArn":{"type":"string"},"repositoryName":{"type":"string"},"repositoryUri":{"type":"string"},"scanOnPush":{"type":["boolean","null"]}},"x-readme-ref-name":"AwsEcrRepositoryHeartbeatData"}},"repositoriesTruncated":{"type":"boolean"},"repositoryCount":{"type":"integer","format":"int32","minimum":0},"repositoryPrefix":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"AwsEcrArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsEcr"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","repositoryId","labelCount","cleanupPolicyCount","kmsKeyNamePresent","iamPolicyEtagPresent","iamBindingCount","iamRoles"],"properties":{"cleanupPolicyCount":{"type":"integer","format":"int32","minimum":0},"cleanupPolicyDryRun":{"type":["boolean","null"]},"createTime":{"type":["string","null"]},"description":{"type":["string","null"]},"format":{"type":["string","null"]},"iamBindingCount":{"type":"integer","format":"int32","minimum":0},"iamPolicyEtagPresent":{"type":"boolean"},"iamRoles":{"type":"array","items":{"type":"string"}},"kmsKeyNamePresent":{"type":"boolean"},"labelCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"mode":{"type":["string","null"]},"name":{"type":["string","null"]},"projectId":{"type":"string"},"pullServiceAccountEmail":{"type":["string","null"]},"pushServiceAccountEmail":{"type":["string","null"]},"repositoryId":{"type":"string"},"satisfiesPzs":{"type":["boolean","null"]},"sizeBytes":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"updateTime":{"type":["string","null"]}},"x-readme-ref-name":"GcpArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpArtifactRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceGroup","location","skuName","adminUserEnabled","anonymousPullEnabled","publicNetworkAccess","networkRuleBypassOptions","ipRuleCount","encryptionKeyVaultUriPresent","encryptionKeyIdentifierPresent","policiesPresent","policyCount","privateEndpointConnectionCount","dataEndpointHostNames","zoneRedundancy","managedTagCount"],"properties":{"adminUserEnabled":{"type":"boolean"},"anonymousPullEnabled":{"type":"boolean"},"creationDate":{"type":["string","null"]},"dataEndpointEnabled":{"type":["boolean","null"]},"dataEndpointHostNames":{"type":"array","items":{"type":"string"}},"encryptionKeyIdentifierPresent":{"type":"boolean"},"encryptionKeyVaultUriPresent":{"type":"boolean"},"encryptionStatus":{"type":["string","null"]},"ipRuleCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"loginServer":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"networkRuleBypassOptions":{"type":"string"},"networkRuleDefaultAction":{"type":["string","null"]},"policiesPresent":{"type":"boolean"},"policyCount":{"type":"integer","format":"int32","minimum":0},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"resourceGroup":{"type":"string"},"resourceId":{"type":["string","null"]},"skuName":{"type":"string"},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"type":{"type":["string","null"]},"zoneRedundancy":{"type":"string"}},"x-readme-ref-name":"AzureContainerRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","registryUrl","reachable"],"properties":{"reachable":{"type":"boolean"},"registryUrl":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"LocalArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ArtifactRegistryHeartbeatData"},"resourceType":{"type":"string","enum":["artifact-registry"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectName","environmentVariableCount","serviceRolePresent","encryptionKeyPresent"],"properties":{"artifactsEncryptionDisabled":{"type":["boolean","null"]},"artifactsType":{"type":["string","null"]},"cloudWatchLogsStatus":{"type":["string","null"]},"computeType":{"type":["string","null"]},"created":{"type":["number","null"],"format":"double"},"description":{"type":["string","null"]},"encryptionKeyPresent":{"type":"boolean"},"environmentImage":{"type":["string","null"]},"environmentType":{"type":["string","null"]},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"imagePullCredentialsType":{"type":["string","null"]},"lastModified":{"type":["number","null"],"format":"double"},"privilegedMode":{"type":["boolean","null"]},"projectArn":{"type":["string","null"]},"projectName":{"type":"string"},"queuedTimeoutInMinutes":{"type":["integer","null"],"format":"int32"},"s3LogsStatus":{"type":["string","null"]},"serviceRolePresent":{"type":"boolean"},"sourceType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"timeoutInMinutes":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"AwsCodeBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsCodeBuild"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","buildConfigId","environmentVariableCount"],"properties":{"buildConfigId":{"type":"string"},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"projectId":{"type":"string"},"serviceAccount":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudBuild"]}}}]},{"allOf":[{"type":"object","required":["status","managedEnvironmentId","resourceGroupName","environmentVariableCount"],"properties":{"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"managedEnvironmentId":{"type":"string"},"managedIdentityId":{"type":["string","null"]},"resourceGroupName":{"type":"string"},"resourcePrefix":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","jobName","namespace","conditionCount","events"],"properties":{"active":{"type":["integer","null"],"format":"int32"},"completionTime":{"type":["string","null"],"format":"date-time"},"conditionCount":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"failed":{"type":["integer","null"],"format":"int32"},"imageDigest":{"type":["string","null"]},"jobName":{"type":"string"},"namespace":{"type":"string"},"startTime":{"type":["string","null"],"format":"date-time"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"succeeded":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"KubernetesBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesJob"]}}}]}],"x-readme-ref-name":"BuildHeartbeatData"},"resourceType":{"type":"string","enum":["build"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectId","serviceName","enabled"],"properties":{"enabled":{"type":"boolean"},"lastOperationName":{"type":["string","null"]},"projectId":{"type":"string"},"serviceName":{"type":"string"},"serviceResourceName":{"type":["string","null"]},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"},"title":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceUsageActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceUsage"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","resourceTypeCount","registered"],"properties":{"namespace":{"type":"string"},"providerId":{"type":["string","null"]},"registered":{"type":"boolean"},"registrationPolicy":{"type":["string","null"]},"registrationState":{"type":["string","null"]},"resourceTypeCount":{"type":"integer","format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceProviderActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureResourceProvider"]}}}]}],"x-readme-ref-name":"ServiceActivationHeartbeatData"},"resourceType":{"type":"string","enum":["service_activation"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","managedTags"],"properties":{"location":{"type":["string","null"]},"managedTags":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatData"},"resourceType":{"type":"string","enum":["azure_resource_group"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","primaryEndpoints","secondaryEndpoints"],"properties":{"allowBlobPublicAccess":{"type":["boolean","null"]},"allowSharedKeyAccess":{"type":["boolean","null"]},"encryptionKeySource":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"networkBypass":{"type":["string","null"]},"networkDefaultAction":{"type":["string","null"]},"networkIpRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkResourceAccessRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkVirtualNetworkRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"primaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"requireInfrastructureEncryption":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secondaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"supportsHttpsTrafficOnly":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureStorageAccountHeartbeatData"},"resourceType":{"type":"string","enum":["azure_storage_account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","workloadProfileCount","workloadProfiles"],"properties":{"customDomainVerificationId":{"type":["string","null"]},"defaultDomain":{"type":["string","null"]},"eventStreamEndpoint":{"type":["string","null"]},"infrastructureResourceGroup":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"staticIp":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatStatus"},"workloadProfileCount":{"type":"integer","format":"int32","minimum":0},"workloadProfiles":{"type":"array","items":{"type":"object","required":["name","workloadProfileType"],"properties":{"maximumCount":{"type":["integer","null"],"format":"int32"},"minimumCount":{"type":["integer","null"],"format":"int32"},"name":{"type":"string"},"workloadProfileType":{"type":"string"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentWorkloadProfile"}},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatData"},"resourceType":{"type":"string","enum":["azure_container_apps_environment"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","privateEndpointConnectionCount"],"properties":{"createdAt":{"type":["string","null"]},"disableLocalAuth":{"type":["boolean","null"]},"location":{"type":["string","null"]},"metricId":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"namespaceStatus":{"type":["string","null"]},"premiumMessagingPartitions":{"type":["integer","null"],"format":"int32"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"serviceBusEndpoint":{"type":["string","null"]},"skuCapacity":{"type":["integer","null"],"format":"int32"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"updatedAt":{"type":["string","null"]},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureServiceBusNamespaceHeartbeatData"},"resourceType":{"type":"string","enum":["azure_service_bus_namespace"]}}}],"x-readme-ref-name":"ResourceHeartbeatData"},"deploymentId":{"type":["string","null"]},"observedAt":{"type":"string","format":"date-time"},"raw":{"type":"array","items":{"type":"object","required":["source","format","collectedAt","body","truncated"],"properties":{"body":{"type":"string"},"collectedAt":{"type":"string","format":"date-time"},"format":{"type":"string","enum":["json","yaml","text"],"x-readme-ref-name":"RawHeartbeatSnippetFormat"},"source":{"type":"string"},"truncated":{"type":"boolean"}},"x-readme-ref-name":"RawHeartbeatSnippet"}},"resourceId":{"type":"string","description":"Alien resource id, such as the `alien.Container` or `alien.Storage`\nresource id from the stack."},"resourceType":{"type":"string","description":"Resource type identifier that determines the specific kind of resource. This field is used for polymorphic deserialization and resource-specific behavior.","examples":["worker","storage","queue","redis","postgres"],"x-readme-ref-name":"ResourceType"}},"x-readme-ref-name":"ResourceHeartbeat"} \ No newline at end of file +{"type":"object","required":["resourceId","resourceType","controllerPlatform","backend","observedAt","data","raw"],"properties":{"backend":{"type":"string","enum":["aws","gcp","azure","kubernetes","local","managed","external","test"],"x-readme-ref-name":"HeartbeatBackend"},"controllerPlatform":{"type":"string","description":"Represents the target cloud platform.","enum":["aws","gcp","azure","kubernetes","machines","local","test"],"x-readme-ref-name":"Platform"},"data":{"oneOf":[{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent","publicAccessBlockPresent"],"properties":{"blockPublicAcls":{"type":["boolean","null"]},"blockPublicPolicy":{"type":["boolean","null"]},"bucketAclPresent":{"type":["boolean","null"]},"bucketLocation":{"type":["string","null"]},"bucketPolicyPresent":{"type":["boolean","null"]},"encryptionConfigPresent":{"type":"boolean"},"encryptionEnabled":{"type":["boolean","null"]},"ignorePublicAcls":{"type":["boolean","null"]},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"publicAccessBlockPresent":{"type":"boolean"},"region":{"type":["string","null"]},"restrictPublicBuckets":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"versioningEnabled":{"type":["boolean","null"]},"versioningStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsS3StorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsS3"]}}}]},{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent"],"properties":{"bucketId":{"type":["string","null"]},"defaultKmsKeyName":{"type":["string","null"]},"encryptionConfigPresent":{"type":"boolean"},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"location":{"type":["string","null"]},"locationType":{"type":["string","null"]},"name":{"type":"string"},"publicAccessPrevention":{"type":["string","null"]},"retentionPeriod":{"type":["string","null"]},"retentionPolicyEffectiveTime":{"type":["string","null"]},"retentionPolicyIsLocked":{"type":["boolean","null"]},"softDeleteEffectiveTime":{"type":["string","null"]},"softDeleteRetentionDurationSeconds":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"storageClass":{"type":["string","null"]},"uniformBucketLevelAccessEnabled":{"type":["boolean","null"]},"uniformBucketLevelAccessLockedTime":{"type":["string","null"]},"versioningEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"GcpCloudStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudStorage"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"accessTier":{"type":["string","null"]},"accountKind":{"type":["string","null"]},"allowBlobPublicAccess":{"type":["boolean","null"]},"blobDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"blobDeleteRetentionEnabled":{"type":["boolean","null"]},"blobEncryptionEnabled":{"type":["boolean","null"]},"blobVersioningEnabled":{"type":["boolean","null"]},"changeFeedEnabled":{"type":["boolean","null"]},"changeFeedRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionEnabled":{"type":["boolean","null"]},"containerPublicAccess":{"type":["string","null"]},"encryptionKeySource":{"type":["string","null"]},"fileEncryptionEnabled":{"type":["boolean","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"primaryLocation":{"type":["string","null"]},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"queueEncryptionEnabled":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"secondaryLocation":{"type":["string","null"]},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"statusOfPrimary":{"type":["string","null"]},"statusOfSecondary":{"type":["string","null"]},"storageAccountName":{"type":["string","null"]},"tableEncryptionEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureBlobStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureBlob"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"}},"x-readme-ref-name":"LocalStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"StorageHeartbeatData"},"resourceType":{"type":"string","enum":["storage"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","functionName","layerCount","functionUrlCorsPresent","triggerCount"],"properties":{"codeSha256":{"type":["string","null"]},"functionName":{"type":"string"},"functionUrlAuthType":{"type":["string","null"]},"functionUrlCorsPresent":{"type":"boolean"},"lastModified":{"type":["string","null"]},"lastUpdateStatus":{"type":["string","null"]},"lastUpdateStatusReason":{"type":["string","null"]},"lastUpdateStatusReasonCode":{"type":["string","null"]},"layerCount":{"type":"integer","format":"int32","minimum":0},"memorySizeMb":{"type":["integer","null"],"format":"int64"},"packageType":{"type":["string","null"]},"revisionId":{"type":["string","null"]},"runtime":{"type":["string","null"]},"state":{"type":["string","null"]},"stateReason":{"type":["string","null"]},"stateReasonCode":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"timeoutSeconds":{"type":["integer","null"],"format":"int64"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"version":{"type":["string","null"]}},"x-readme-ref-name":"AwsLambdaWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsLambda"]}}}]},{"allOf":[{"type":"object","required":["status","service","urls","trafficCount"],"properties":{"containerImage":{"type":["string","null"]},"cpuLimit":{"type":["string","null"]},"generation":{"type":["integer","null"],"format":"int64"},"latestCreatedRevision":{"type":["string","null"]},"latestReadyRevision":{"type":["string","null"]},"maxInstanceCount":{"type":["integer","null"],"format":"int32"},"memoryLimit":{"type":["string","null"]},"minInstanceCount":{"type":["integer","null"],"format":"int32"},"observedGeneration":{"type":["integer","null"],"format":"int64"},"region":{"type":["string","null"]},"service":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"trafficCount":{"type":"integer","format":"int32","minimum":0},"uri":{"type":["string","null"]},"urls":{"type":"array","items":{"type":"string"}}},"x-readme-ref-name":"GcpCloudRunWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudRun"]}}}]},{"allOf":[{"type":"object","required":["status","appName"],"properties":{"appName":{"type":"string"},"cpu":{"type":["number","null"],"format":"double"},"environmentName":{"type":["string","null"]},"ingressFqdn":{"type":["string","null"]},"maxReplicas":{"type":["integer","null"],"format":"int32"},"memory":{"type":["string","null"]},"minReplicas":{"type":["integer","null"],"format":"int32"},"provisioningState":{"type":["string","null"]},"revision":{"type":["string","null"]},"runningStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","triggerCount","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","commandSupported","imagePathPresent","triggerCount","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"imagePathPresent":{"type":"boolean"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"process":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"readinessProbeOk":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"LocalWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"WorkerHeartbeatData"},"resourceType":{"type":"string","enum":["worker"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","containerId","schedulingMode","replicas","attentionCount","replicaUnits","events"],"properties":{"attentionCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":"string"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"latestUpdateTimestamp":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"observedImage":{"type":["string","null"]},"replicaUnits":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"schedulingMode":{"type":"string","enum":["replicated","stateful","daemon"],"x-readme-ref-name":"HorizonWorkloadSchedulingMode"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"HorizonContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["horizonPlatform"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","portCount","bindMountCount","runtimeReachable","events"],"properties":{"bindMountCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":["string","null"]},"containerUnit":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"localUrl":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":["string","null"]},"portCount":{"type":"integer","format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeReachable":{"type":"boolean"},"runtimeStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ContainerHeartbeatData"},"resourceType":{"type":"string","enum":["container"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"GcpDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AzureDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"MachinesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","replicas","commandSupported","pods","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]}},"x-readme-ref-name":"KubernetesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","runtimeId","commandSupported","imagePathPresent","events"],"properties":{"commandSupported":{"type":"boolean"},"daemonInstance":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"daemonName":{"type":"string"},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"exitReason":{"type":["string","null"]},"imagePathPresent":{"type":"boolean"},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeId":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"DaemonHeartbeatData"},"resourceType":{"type":"string","enum":["daemon"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AwsComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"GcpComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AzureComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","machines"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"machines":{"type":"array","items":{"type":"object","required":["machineId","status","capacityGroup","zone","lastHeartbeat","replicaCount","drainForce"],"properties":{"capacityGroup":{"type":"string"},"cpuCores":{"type":["number","null"],"format":"double"},"drainBlockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainForce":{"type":"boolean"},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"horizondVersion":{"type":["string","null"]},"lastHeartbeat":{"type":"string"},"machineId":{"type":"string"},"memoryBytes":{"type":["integer","null"],"format":"int64"},"overlayIp":{"type":["string","null"]},"publicIp":{"type":["string","null"]},"replicaCount":{"type":"integer","format":"int64"},"status":{"type":"string"},"zone":{"type":"string"}},"x-readme-ref-name":"MachinesComputeMachineStatus"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"MachinesComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","dockerAvailable","networkAvailable"],"properties":{"dockerApiVersion":{"type":["string","null"]},"dockerArch":{"type":["string","null"]},"dockerAvailable":{"type":"boolean"},"dockerOs":{"type":["string","null"]},"dockerVersion":{"type":["string","null"]},"hostIdentifier":{"type":["string","null"]},"name":{"type":"string"},"networkAvailable":{"type":"boolean"},"networkName":{"type":["string","null"]},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"runningContainers":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"},"trackedContainers":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"LocalComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ComputeClusterHeartbeatData"},"resourceType":{"type":"string","enum":["compute-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","nodeCounts","podCounts","name","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":["string","null"]},"nodeCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"nodeStatuses":{"type":"array","items":{"type":"object","required":["name","ready","roles","labels","allocatable","capacity"],"properties":{"allocatable":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"capacity":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesNodeConditionStatus"}},"containerRuntimeVersion":{"type":["string","null"]},"kubeletVersion":{"type":["string","null"]},"labels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"ready":{"type":"boolean"},"roles":{"type":"array","items":{"type":"string"}},"uid":{"type":["string","null"]},"usage":{"oneOf":[{"type":"null"},{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"KubernetesNodeUsage"}]}},"x-readme-ref-name":"KubernetesClusterNodeStatus"}},"podCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesClusterHeartbeatData"},"resourceType":{"type":"string","enum":["kubernetes-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","approximateCounts"],"properties":{"approximateCounts":{"type":"boolean"},"approximateDelayedMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateInFlightMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateVisibleMessages":{"type":["integer","null"],"format":"int64","minimum":0},"contentBasedDeduplication":{"type":["boolean","null"]},"deduplicationScope":{"type":["string","null"]},"delaySeconds":{"type":["integer","null"],"format":"int32","minimum":0},"fifoQueue":{"type":["boolean","null"]},"fifoThroughputLimit":{"type":["string","null"]},"kmsDataKeyReusePeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"kmsMasterKeyId":{"type":["string","null"]},"maximumMessageSize":{"type":["integer","null"],"format":"int32","minimum":0},"messageRetentionPeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"queueArn":{"type":["string","null"]},"queueUrl":{"type":["string","null"]},"receiveMessageWaitTimeSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"redriveAllowPolicy":{"type":["string","null"]},"redrivePolicy":{"type":["string","null"]},"region":{"type":["string","null"]},"sqsManagedSseEnabled":{"type":["boolean","null"]},"sseEnabled":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"visibilityTimeoutSeconds":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"AwsSqsQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsSqs"]}}}]},{"allOf":[{"type":"object","required":["status","topicName","topicLabels","subscriptionLabels","messageStorageAllowedPersistenceRegions","subscriptionPushAttributes"],"properties":{"endpoint":{"type":["string","null"]},"kmsKeyName":{"type":["string","null"]},"messageStorageAllowedPersistenceRegions":{"type":"array","items":{"type":"string"}},"messageStorageEnforceInTransit":{"type":["boolean","null"]},"projectId":{"type":["string","null"]},"schemaEncoding":{"type":["string","null"]},"schemaFirstRevisionId":{"type":["string","null"]},"schemaLastRevisionId":{"type":["string","null"]},"schemaName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"subscriptionAckDeadlineSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterMaxDeliveryAttempts":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterTopic":{"type":["string","null"]},"subscriptionDetached":{"type":["boolean","null"]},"subscriptionEnableMessageOrdering":{"type":["boolean","null"]},"subscriptionFilter":{"type":["string","null"]},"subscriptionFullName":{"type":["string","null"]},"subscriptionLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionMessageRetentionDuration":{"type":["string","null"]},"subscriptionName":{"type":["string","null"]},"subscriptionPushAttributes":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionPushConfigPresent":{"type":["boolean","null"]},"subscriptionPushEndpoint":{"type":["string","null"]},"subscriptionPushNoWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionPushOidcAudience":{"type":["string","null"]},"subscriptionPushOidcServiceAccountEmail":{"type":["string","null"]},"subscriptionPushPubsubWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionRetainAckedMessages":{"type":["boolean","null"]},"subscriptionState":{"type":["string","null"]},"topicFullName":{"type":["string","null"]},"topicLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"topicMessageRetentionDuration":{"type":["string","null"]},"topicName":{"type":"string"},"topicState":{"type":["string","null"]}},"x-readme-ref-name":"GcpPubSubQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpPubSub"]}}}]},{"allOf":[{"type":"object","required":["status","name","namespaceName"],"properties":{"accessedAt":{"type":["string","null"]},"activeMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"autoDeleteOnIdle":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"deadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"deadLetteringOnMessageExpiration":{"type":["boolean","null"]},"defaultMessageTimeToLive":{"type":["string","null"]},"duplicateDetectionHistoryTimeWindow":{"type":["string","null"]},"enableBatchedOperations":{"type":["boolean","null"]},"enableExpress":{"type":["boolean","null"]},"enablePartitioning":{"type":["boolean","null"]},"endpoint":{"type":["string","null"]},"forwardDeadLetteredMessagesTo":{"type":["string","null"]},"forwardTo":{"type":["string","null"]},"lockDuration":{"type":["string","null"]},"maxDeliveryCount":{"type":["integer","null"],"format":"int32","minimum":0},"maxMessageSizeInKilobytes":{"type":["integer","null"],"format":"int64","minimum":0},"maxSizeInMegabytes":{"type":["integer","null"],"format":"int32","minimum":0},"messageCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"namespaceName":{"type":"string"},"queueStatus":{"type":["string","null"]},"requiresDuplicateDetection":{"type":["boolean","null"]},"requiresSession":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"scheduledMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"sizeInBytes":{"type":["integer","null"],"format":"int64","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"transferDeadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"transferMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"updatedAt":{"type":["string","null"]}},"x-readme-ref-name":"AzureServiceBusQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureServiceBus"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"name":{"type":"string"},"path":{"type":["string","null"]},"serviceStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"}},"x-readme-ref-name":"LocalQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"QueueHeartbeatData"},"resourceType":{"type":"string","enum":["queue"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","keySchema"],"properties":{"billingMode":{"type":["string","null"]},"deletionProtectionEnabled":{"type":["boolean","null"]},"globalSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"itemCount":{"type":["integer","null"],"format":"int64","minimum":0},"keySchema":{"type":"array","items":{"type":"object","required":["attributeName","keyType"],"properties":{"attributeName":{"type":"string"},"keyType":{"type":"string"}},"x-readme-ref-name":"AwsDynamoDbKeySchemaElement"}},"localSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"region":{"type":["string","null"]},"replicaCount":{"type":["integer","null"],"format":"int32","minimum":0},"restoreInProgress":{"type":["boolean","null"]},"sseStatus":{"type":["string","null"]},"sseType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"streamEnabled":{"type":["boolean","null"]},"streamViewType":{"type":["string","null"]},"tableArn":{"type":["string","null"]},"tableClass":{"type":["string","null"]},"tableSizeBytes":{"type":["integer","null"],"format":"int64","minimum":0},"tableStatus":{"type":["string","null"]},"ttlAttributeName":{"type":["string","null"]},"ttlStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsDynamoDbKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsDynamoDb"]}}}]},{"allOf":[{"type":"object","required":["status","databaseName","cmekEnabled","sourceInfoPresent"],"properties":{"appEngineIntegrationMode":{"type":["string","null"]},"cmekEnabled":{"type":"boolean"},"concurrencyMode":{"type":["string","null"]},"createTime":{"type":["string","null"]},"databaseEdition":{"type":["string","null"]},"databaseName":{"type":"string"},"databaseType":{"type":["string","null"]},"deleteProtectionState":{"type":["string","null"]},"deleteTime":{"type":["string","null"]},"earliestVersionTime":{"type":["string","null"]},"endpoint":{"type":["string","null"]},"locationId":{"type":["string","null"]},"pointInTimeRecoveryEnablement":{"type":["string","null"]},"projectId":{"type":["string","null"]},"sourceInfoPresent":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"updateTime":{"type":["string","null"]},"versionRetentionPeriod":{"type":["string","null"]}},"x-readme-ref-name":"GcpFirestoreKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpFirestore"]}}}]},{"allOf":[{"type":"object","required":["status","tableName","storageAccountName","tableExists"],"properties":{"endpoint":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"signedIdentifierCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"storageAccountKind":{"type":["string","null"]},"storageAccountLocation":{"type":["string","null"]},"storageAccountName":{"type":"string"},"storageAccountPrimaryStatus":{"type":["string","null"]},"storageAccountProvisioningState":{"type":["string","null"]},"storageAccountResourceId":{"type":["string","null"]},"tableExists":{"type":"boolean"},"tableName":{"type":"string"}},"x-readme-ref-name":"AzureTableKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureTable"]}}}]},{"allOf":[{"type":"object","required":["status","name","path","pathExists","cloudMetadataSupported"],"properties":{"cloudMetadataSupported":{"type":"boolean"},"isDirectory":{"type":["boolean","null"]},"name":{"type":"string"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"}},"x-readme-ref-name":"LocalKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"KvHeartbeatData"},"resourceType":{"type":"string","enum":["kv"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"description":"AWS Aurora Serverless v2 backend.","type":"object","required":["status","clusterIdentifier","neverPauses"],"properties":{"clusterIdentifier":{"type":"string"},"endpoint":{"type":["string","null"]},"engineVersion":{"type":["string","null"]},"neverPauses":{"type":"boolean","description":"True when a `minCapacity: 0` instance has not reached 0 ACU over the observation\nwindow — it is silently paying always-on prices (auto-pause verification)."},"serverlessCapacity":{"type":["number","null"],"format":"double","description":"Latest sampled `ServerlessDatabaseCapacity` (ACU)."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"AuroraPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aurora"]}}}],"description":"AWS Aurora Serverless v2 backend."},{"allOf":[{"description":"GCP Cloud SQL backend.","type":"object","required":["status","instanceName"],"properties":{"databaseVersion":{"type":["string","null"]},"instanceName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudSqlPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["cloudSql"]}}}],"description":"GCP Cloud SQL backend."},{"allOf":[{"description":"Azure Flexible Server backend.","type":"object","required":["status","serverName"],"properties":{"serverName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"AzureFlexibleServerPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["flexibleServer"]}}}],"description":"Azure Flexible Server backend."},{"allOf":[{"description":"Local embedded Postgres backend.","type":"object","required":["status","name","version","processRunning"],"properties":{"name":{"type":"string"},"port":{"type":["integer","null"],"format":"int32","minimum":0},"processRunning":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":"string"}},"x-readme-ref-name":"LocalPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}],"description":"Local embedded Postgres backend."}],"x-readme-ref-name":"PostgresHeartbeatData"},"resourceType":{"type":"string","enum":["postgres"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","accountId","region","prefix","parameterMetadataSampled"],"properties":{"accountId":{"type":"string"},"hasMoreParameters":{"type":["boolean","null"]},"latestModifiedAt":{"type":["string","null"],"format":"date-time"},"parameterMetadataSampled":{"type":"boolean"},"prefix":{"type":"string"},"region":{"type":"string"},"sampledAdvancedTierCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledKmsKeyMetadataPresentCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledParameterCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledSecureStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringListCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"AwsParameterStoreVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsParameterStore"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","prefix","secretMetadataListed"],"properties":{"location":{"type":"string"},"prefix":{"type":"string"},"projectId":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"GcpSecretManagerVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpSecretManager"]}}}]},{"allOf":[{"type":"object","required":["status","name","softDeleteEnabled","softDeleteRetentionDays","rbacAuthorizationEnabled","publicNetworkAccess","accessPolicyCount","privateEndpointConnectionCount","secretMetadataListed"],"properties":{"accessPolicyCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":["string","null"]},"name":{"type":"string"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"purgeProtectionEnabled":{"type":["boolean","null"]},"rbacAuthorizationEnabled":{"type":"boolean"},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secretMetadataListed":{"type":"boolean"},"skuFamily":{"type":["string","null"]},"skuName":{"type":["string","null"]},"softDeleteEnabled":{"type":"boolean"},"softDeleteRetentionDays":{"type":"integer","format":"int32"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"},"vaultUri":{"type":["string","null"]}},"x-readme-ref-name":"AzureKeyVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureKeyVault"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","prefix","secretMetadataListed"],"properties":{"namespace":{"type":"string"},"prefix":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"KubernetesSecretVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesSecret"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists","secretMetadataListed"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"LocalVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"VaultHeartbeatData"},"resourceType":{"type":"string","enum":["vault"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","roleName","roleArn","roleId","path","createDate","assumeRolePolicyPresent","tagCount","managedTagCount","attachedPolicyCount","attachedPolicyNames","inlinePolicyCount","inlinePolicyNames","stackPermissionsApplied"],"properties":{"assumeRolePolicyPresent":{"type":"boolean"},"attachedPolicyCount":{"type":"integer","format":"int32","minimum":0},"attachedPolicyNames":{"type":"array","items":{"type":"string"}},"createDate":{"type":"string"},"description":{"type":["string","null"]},"inlinePolicyCount":{"type":"integer","format":"int32","minimum":0},"inlinePolicyNames":{"type":"array","items":{"type":"string"}},"lastUsedDate":{"type":["string","null"]},"lastUsedRegion":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"maxSessionDuration":{"type":["integer","null"],"format":"int32"},"path":{"type":"string"},"permissionsBoundaryArn":{"type":["string","null"]},"permissionsBoundaryType":{"type":["string","null"]},"roleArn":{"type":"string"},"roleId":{"type":"string"},"roleName":{"type":"string"},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tagCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsIamRoleServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","email","projectBindingCount","projectRoles","serviceAccountBindingCount","serviceAccountRoles"],"properties":{"description":{"type":["string","null"]},"disabled":{"type":["boolean","null"]},"displayName":{"type":["string","null"]},"email":{"type":"string"},"etag":{"type":["string","null"]},"name":{"type":["string","null"]},"oauth2ClientId":{"type":["string","null"]},"projectBindingCount":{"type":"integer","format":"int32","minimum":0},"projectId":{"type":["string","null"]},"projectRoles":{"type":"array","items":{"type":"string"}},"serviceAccountBindingCount":{"type":"integer","format":"int32","minimum":0},"serviceAccountRoles":{"type":"array","items":{"type":"string"}},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"uniqueId":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceId","resourceGroup","location","managedTagCount","roleAssignmentCount","roleAssignmentIds","customRoleDefinitionCount","customRoleDefinitionIds","stackPermissionsApplied"],"properties":{"clientId":{"type":["string","null"]},"customRoleDefinitionCount":{"type":"integer","format":"int32","minimum":0},"customRoleDefinitionIds":{"type":"array","items":{"type":"string"}},"isolationScope":{"type":["string","null"]},"location":{"type":"string"},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"principalId":{"type":["string","null"]},"resourceGroup":{"type":"string"},"resourceId":{"type":"string"},"roleAssignmentCount":{"type":"integer","format":"int32","minimum":0},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tenantId":{"type":["string","null"]},"type":{"type":["string","null"]}},"x-readme-ref-name":"AzureManagedIdentityServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]},{"allOf":[{"type":"object","required":["status","identity","configured"],"properties":{"configured":{"type":"boolean"},"identity":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"}},"x-readme-ref-name":"LocalServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ServiceAccountHeartbeatData"},"resourceType":{"type":"string","enum":["service-account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","publicSubnetIds","privateSubnetIds","availabilityZones","routeTableCount","isByoVpc"],"properties":{"availabilityZones":{"type":"array","items":{"type":"string"}},"cidrBlock":{"type":["string","null"]},"internetGatewayId":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"natGatewayId":{"type":["string","null"]},"privateSubnetIds":{"type":"array","items":{"type":"string"}},"publicSubnetIds":{"type":"array","items":{"type":"string"}},"routeTableCount":{"type":"integer","format":"int32","minimum":0},"securityGroupId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vpcId":{"type":["string","null"]},"vpcState":{"type":["string","null"]}},"x-readme-ref-name":"AwsVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVpc"],"properties":{"cidrBlock":{"type":["string","null"]},"cloudNatName":{"type":["string","null"]},"firewallName":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"networkName":{"type":["string","null"]},"networkSelfLink":{"type":["string","null"]},"region":{"type":["string","null"]},"routerName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"subnetworkName":{"type":["string","null"]},"subnetworkSelfLink":{"type":["string","null"]}},"x-readme-ref-name":"GcpVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVnet"],"properties":{"applicationGatewaySubnetName":{"type":["string","null"]},"cidrBlock":{"type":["string","null"]},"isByoVnet":{"type":"boolean"},"lastByoVnetVerificationErrorCode":{"type":["string","null"]},"location":{"type":["string","null"]},"natGatewayId":{"type":["string","null"]},"nsgId":{"type":["string","null"]},"privateEndpointSubnetName":{"type":["string","null"]},"privateSubnetName":{"type":["string","null"]},"publicIpId":{"type":["string","null"]},"publicSubnetName":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vnetName":{"type":["string","null"]},"vnetResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureVnetNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureVnet"]}}}]}],"x-readme-ref-name":"NetworkHeartbeatData"},"resourceType":{"type":"string","enum":["network"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","managementPermissionsApplied"],"properties":{"managementPermissionsApplied":{"type":"boolean"},"roleArn":{"type":["string","null"]},"roleName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"AwsRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","roleBound","impersonationGranted"],"properties":{"impersonationGranted":{"type":"boolean"},"roleBound":{"type":"boolean"},"serviceAccountEmail":{"type":["string","null"]},"serviceAccountUniqueId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"GcpRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","roleAssignmentIds"],"properties":{"ficName":{"type":["string","null"]},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"roleDefinitionId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"},"tenantId":{"type":["string","null"]},"uamiClientId":{"type":["string","null"]},"uamiPrincipalId":{"type":["string","null"]},"uamiResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]}],"x-readme-ref-name":"RemoteStackManagementHeartbeatData"},"resourceType":{"type":"string","enum":["remote-stack-management"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","registryId","region","registryUri","repositoryPrefix","repositoryCount","repositoriesTruncated","repositories"],"properties":{"pullRoleArn":{"type":["string","null"]},"pushRoleArn":{"type":["string","null"]},"region":{"type":"string"},"registryId":{"type":"string"},"registryUri":{"type":"string"},"repositories":{"type":"array","items":{"type":"object","required":["repositoryArn","registryId","repositoryName","repositoryUri","createdAt","kmsKeyPresent"],"properties":{"createdAt":{"type":"number","format":"double"},"encryptionType":{"type":["string","null"]},"imageTagMutability":{"type":["string","null"]},"kmsKeyPresent":{"type":"boolean"},"registryId":{"type":"string"},"repositoryArn":{"type":"string"},"repositoryName":{"type":"string"},"repositoryUri":{"type":"string"},"scanOnPush":{"type":["boolean","null"]}},"x-readme-ref-name":"AwsEcrRepositoryHeartbeatData"}},"repositoriesTruncated":{"type":"boolean"},"repositoryCount":{"type":"integer","format":"int32","minimum":0},"repositoryPrefix":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"AwsEcrArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsEcr"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","repositoryId","labelCount","cleanupPolicyCount","kmsKeyNamePresent","iamPolicyEtagPresent","iamBindingCount","iamRoles"],"properties":{"cleanupPolicyCount":{"type":"integer","format":"int32","minimum":0},"cleanupPolicyDryRun":{"type":["boolean","null"]},"createTime":{"type":["string","null"]},"description":{"type":["string","null"]},"format":{"type":["string","null"]},"iamBindingCount":{"type":"integer","format":"int32","minimum":0},"iamPolicyEtagPresent":{"type":"boolean"},"iamRoles":{"type":"array","items":{"type":"string"}},"kmsKeyNamePresent":{"type":"boolean"},"labelCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"mode":{"type":["string","null"]},"name":{"type":["string","null"]},"projectId":{"type":"string"},"pullServiceAccountEmail":{"type":["string","null"]},"pushServiceAccountEmail":{"type":["string","null"]},"repositoryId":{"type":"string"},"satisfiesPzs":{"type":["boolean","null"]},"sizeBytes":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"updateTime":{"type":["string","null"]}},"x-readme-ref-name":"GcpArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpArtifactRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceGroup","location","skuName","adminUserEnabled","anonymousPullEnabled","publicNetworkAccess","networkRuleBypassOptions","ipRuleCount","encryptionKeyVaultUriPresent","encryptionKeyIdentifierPresent","policiesPresent","policyCount","privateEndpointConnectionCount","dataEndpointHostNames","zoneRedundancy","managedTagCount"],"properties":{"adminUserEnabled":{"type":"boolean"},"anonymousPullEnabled":{"type":"boolean"},"creationDate":{"type":["string","null"]},"dataEndpointEnabled":{"type":["boolean","null"]},"dataEndpointHostNames":{"type":"array","items":{"type":"string"}},"encryptionKeyIdentifierPresent":{"type":"boolean"},"encryptionKeyVaultUriPresent":{"type":"boolean"},"encryptionStatus":{"type":["string","null"]},"ipRuleCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"loginServer":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"networkRuleBypassOptions":{"type":"string"},"networkRuleDefaultAction":{"type":["string","null"]},"policiesPresent":{"type":"boolean"},"policyCount":{"type":"integer","format":"int32","minimum":0},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"resourceGroup":{"type":"string"},"resourceId":{"type":["string","null"]},"skuName":{"type":"string"},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"type":{"type":["string","null"]},"zoneRedundancy":{"type":"string"}},"x-readme-ref-name":"AzureContainerRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","registryUrl","reachable"],"properties":{"reachable":{"type":"boolean"},"registryUrl":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"LocalArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ArtifactRegistryHeartbeatData"},"resourceType":{"type":"string","enum":["artifact-registry"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectName","environmentVariableCount","serviceRolePresent","encryptionKeyPresent"],"properties":{"artifactsEncryptionDisabled":{"type":["boolean","null"]},"artifactsType":{"type":["string","null"]},"cloudWatchLogsStatus":{"type":["string","null"]},"computeType":{"type":["string","null"]},"created":{"type":["number","null"],"format":"double"},"description":{"type":["string","null"]},"encryptionKeyPresent":{"type":"boolean"},"environmentImage":{"type":["string","null"]},"environmentType":{"type":["string","null"]},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"imagePullCredentialsType":{"type":["string","null"]},"lastModified":{"type":["number","null"],"format":"double"},"privilegedMode":{"type":["boolean","null"]},"projectArn":{"type":["string","null"]},"projectName":{"type":"string"},"queuedTimeoutInMinutes":{"type":["integer","null"],"format":"int32"},"s3LogsStatus":{"type":["string","null"]},"serviceRolePresent":{"type":"boolean"},"sourceType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"timeoutInMinutes":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"AwsCodeBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsCodeBuild"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","buildConfigId","environmentVariableCount"],"properties":{"buildConfigId":{"type":"string"},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"projectId":{"type":"string"},"serviceAccount":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudBuild"]}}}]},{"allOf":[{"type":"object","required":["status","managedEnvironmentId","resourceGroupName","environmentVariableCount"],"properties":{"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"managedEnvironmentId":{"type":"string"},"managedIdentityId":{"type":["string","null"]},"resourceGroupName":{"type":"string"},"resourcePrefix":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","jobName","namespace","conditionCount","events"],"properties":{"active":{"type":["integer","null"],"format":"int32"},"completionTime":{"type":["string","null"],"format":"date-time"},"conditionCount":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"failed":{"type":["integer","null"],"format":"int32"},"imageDigest":{"type":["string","null"]},"jobName":{"type":"string"},"namespace":{"type":"string"},"startTime":{"type":["string","null"],"format":"date-time"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"succeeded":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"KubernetesBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesJob"]}}}]}],"x-readme-ref-name":"BuildHeartbeatData"},"resourceType":{"type":"string","enum":["build"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectId","serviceName","enabled"],"properties":{"enabled":{"type":"boolean"},"lastOperationName":{"type":["string","null"]},"projectId":{"type":"string"},"serviceName":{"type":"string"},"serviceResourceName":{"type":["string","null"]},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"},"title":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceUsageActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceUsage"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","resourceTypeCount","registered"],"properties":{"namespace":{"type":"string"},"providerId":{"type":["string","null"]},"registered":{"type":"boolean"},"registrationPolicy":{"type":["string","null"]},"registrationState":{"type":["string","null"]},"resourceTypeCount":{"type":"integer","format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceProviderActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureResourceProvider"]}}}]}],"x-readme-ref-name":"ServiceActivationHeartbeatData"},"resourceType":{"type":"string","enum":["service_activation"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","managedTags"],"properties":{"location":{"type":["string","null"]},"managedTags":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatData"},"resourceType":{"type":"string","enum":["azure_resource_group"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","primaryEndpoints","secondaryEndpoints"],"properties":{"allowBlobPublicAccess":{"type":["boolean","null"]},"allowSharedKeyAccess":{"type":["boolean","null"]},"encryptionKeySource":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"networkBypass":{"type":["string","null"]},"networkDefaultAction":{"type":["string","null"]},"networkIpRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkResourceAccessRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkVirtualNetworkRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"primaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"requireInfrastructureEncryption":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secondaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"supportsHttpsTrafficOnly":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureStorageAccountHeartbeatData"},"resourceType":{"type":"string","enum":["azure_storage_account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","workloadProfileCount","workloadProfiles"],"properties":{"customDomainVerificationId":{"type":["string","null"]},"defaultDomain":{"type":["string","null"]},"eventStreamEndpoint":{"type":["string","null"]},"infrastructureResourceGroup":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"staticIp":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatStatus"},"workloadProfileCount":{"type":"integer","format":"int32","minimum":0},"workloadProfiles":{"type":"array","items":{"type":"object","required":["name","workloadProfileType"],"properties":{"maximumCount":{"type":["integer","null"],"format":"int32"},"minimumCount":{"type":["integer","null"],"format":"int32"},"name":{"type":"string"},"workloadProfileType":{"type":"string"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentWorkloadProfile"}},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatData"},"resourceType":{"type":"string","enum":["azure_container_apps_environment"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","privateEndpointConnectionCount"],"properties":{"createdAt":{"type":["string","null"]},"disableLocalAuth":{"type":["boolean","null"]},"location":{"type":["string","null"]},"metricId":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"namespaceStatus":{"type":["string","null"]},"premiumMessagingPartitions":{"type":["integer","null"],"format":"int32"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"serviceBusEndpoint":{"type":["string","null"]},"skuCapacity":{"type":["integer","null"],"format":"int32"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"updatedAt":{"type":["string","null"]},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureServiceBusNamespaceHeartbeatData"},"resourceType":{"type":"string","enum":["azure_service_bus_namespace"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","region"],"properties":{"region":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AwsBedrockAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsBedrock"]}}}]},{"allOf":[{"type":"object","required":["status","project","location"],"properties":{"location":{"type":"string"},"project":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"GcpVertexAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpVertex"]}}}]},{"allOf":[{"type":"object","required":["status","accountName"],"properties":{"accountName":{"type":"string"},"endpoint":{"type":["string","null"]},"location":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AzureFoundryAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureFoundry"]}}}]},{"allOf":[{"type":"object","required":["status","provider"],"properties":{"provider":{"type":"string","description":"The BYO-key provider serving this binding (e.g. \"openai\"). Used on the Local\nplatform, where the app brings its own provider key instead of an ambient cloud."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"ExternalAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["external"]}}}]}],"x-readme-ref-name":"AiHeartbeatData"},"resourceType":{"type":"string","enum":["ai"]}}}],"x-readme-ref-name":"ResourceHeartbeatData"},"deploymentId":{"type":["string","null"]},"observedAt":{"type":"string","format":"date-time"},"raw":{"type":"array","items":{"type":"object","required":["source","format","collectedAt","body","truncated"],"properties":{"body":{"type":"string"},"collectedAt":{"type":"string","format":"date-time"},"format":{"type":"string","enum":["json","yaml","text"],"x-readme-ref-name":"RawHeartbeatSnippetFormat"},"source":{"type":"string"},"truncated":{"type":"boolean"}},"x-readme-ref-name":"RawHeartbeatSnippet"}},"resourceId":{"type":"string","description":"Alien resource id, such as the `alien.Container` or `alien.Storage`\nresource id from the stack."},"resourceType":{"type":"string","description":"Resource type identifier that determines the specific kind of resource. This field is used for polymorphic deserialization and resource-specific behavior.","examples":["worker","storage","queue","redis","postgres"],"x-readme-ref-name":"ResourceType"}},"x-readme-ref-name":"ResourceHeartbeat"} \ No newline at end of file diff --git a/packages/core/src/generated/schemas/resourceHeartbeatData.json b/packages/core/src/generated/schemas/resourceHeartbeatData.json index c2012cf9e..b4a3eca10 100644 --- a/packages/core/src/generated/schemas/resourceHeartbeatData.json +++ b/packages/core/src/generated/schemas/resourceHeartbeatData.json @@ -1 +1 @@ -{"oneOf":[{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent","publicAccessBlockPresent"],"properties":{"blockPublicAcls":{"type":["boolean","null"]},"blockPublicPolicy":{"type":["boolean","null"]},"bucketAclPresent":{"type":["boolean","null"]},"bucketLocation":{"type":["string","null"]},"bucketPolicyPresent":{"type":["boolean","null"]},"encryptionConfigPresent":{"type":"boolean"},"encryptionEnabled":{"type":["boolean","null"]},"ignorePublicAcls":{"type":["boolean","null"]},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"publicAccessBlockPresent":{"type":"boolean"},"region":{"type":["string","null"]},"restrictPublicBuckets":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"versioningEnabled":{"type":["boolean","null"]},"versioningStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsS3StorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsS3"]}}}]},{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent"],"properties":{"bucketId":{"type":["string","null"]},"defaultKmsKeyName":{"type":["string","null"]},"encryptionConfigPresent":{"type":"boolean"},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"location":{"type":["string","null"]},"locationType":{"type":["string","null"]},"name":{"type":"string"},"publicAccessPrevention":{"type":["string","null"]},"retentionPeriod":{"type":["string","null"]},"retentionPolicyEffectiveTime":{"type":["string","null"]},"retentionPolicyIsLocked":{"type":["boolean","null"]},"softDeleteEffectiveTime":{"type":["string","null"]},"softDeleteRetentionDurationSeconds":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"storageClass":{"type":["string","null"]},"uniformBucketLevelAccessEnabled":{"type":["boolean","null"]},"uniformBucketLevelAccessLockedTime":{"type":["string","null"]},"versioningEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"GcpCloudStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudStorage"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"accessTier":{"type":["string","null"]},"accountKind":{"type":["string","null"]},"allowBlobPublicAccess":{"type":["boolean","null"]},"blobDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"blobDeleteRetentionEnabled":{"type":["boolean","null"]},"blobEncryptionEnabled":{"type":["boolean","null"]},"blobVersioningEnabled":{"type":["boolean","null"]},"changeFeedEnabled":{"type":["boolean","null"]},"changeFeedRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionEnabled":{"type":["boolean","null"]},"containerPublicAccess":{"type":["string","null"]},"encryptionKeySource":{"type":["string","null"]},"fileEncryptionEnabled":{"type":["boolean","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"primaryLocation":{"type":["string","null"]},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"queueEncryptionEnabled":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"secondaryLocation":{"type":["string","null"]},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"statusOfPrimary":{"type":["string","null"]},"statusOfSecondary":{"type":["string","null"]},"storageAccountName":{"type":["string","null"]},"tableEncryptionEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureBlobStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureBlob"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"}},"x-readme-ref-name":"LocalStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"StorageHeartbeatData"},"resourceType":{"type":"string","enum":["storage"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","functionName","layerCount","functionUrlCorsPresent","triggerCount"],"properties":{"codeSha256":{"type":["string","null"]},"functionName":{"type":"string"},"functionUrlAuthType":{"type":["string","null"]},"functionUrlCorsPresent":{"type":"boolean"},"lastModified":{"type":["string","null"]},"lastUpdateStatus":{"type":["string","null"]},"lastUpdateStatusReason":{"type":["string","null"]},"lastUpdateStatusReasonCode":{"type":["string","null"]},"layerCount":{"type":"integer","format":"int32","minimum":0},"memorySizeMb":{"type":["integer","null"],"format":"int64"},"packageType":{"type":["string","null"]},"revisionId":{"type":["string","null"]},"runtime":{"type":["string","null"]},"state":{"type":["string","null"]},"stateReason":{"type":["string","null"]},"stateReasonCode":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"timeoutSeconds":{"type":["integer","null"],"format":"int64"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"version":{"type":["string","null"]}},"x-readme-ref-name":"AwsLambdaWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsLambda"]}}}]},{"allOf":[{"type":"object","required":["status","service","urls","trafficCount"],"properties":{"containerImage":{"type":["string","null"]},"cpuLimit":{"type":["string","null"]},"generation":{"type":["integer","null"],"format":"int64"},"latestCreatedRevision":{"type":["string","null"]},"latestReadyRevision":{"type":["string","null"]},"maxInstanceCount":{"type":["integer","null"],"format":"int32"},"memoryLimit":{"type":["string","null"]},"minInstanceCount":{"type":["integer","null"],"format":"int32"},"observedGeneration":{"type":["integer","null"],"format":"int64"},"region":{"type":["string","null"]},"service":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"trafficCount":{"type":"integer","format":"int32","minimum":0},"uri":{"type":["string","null"]},"urls":{"type":"array","items":{"type":"string"}}},"x-readme-ref-name":"GcpCloudRunWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudRun"]}}}]},{"allOf":[{"type":"object","required":["status","appName"],"properties":{"appName":{"type":"string"},"cpu":{"type":["number","null"],"format":"double"},"environmentName":{"type":["string","null"]},"ingressFqdn":{"type":["string","null"]},"maxReplicas":{"type":["integer","null"],"format":"int32"},"memory":{"type":["string","null"]},"minReplicas":{"type":["integer","null"],"format":"int32"},"provisioningState":{"type":["string","null"]},"revision":{"type":["string","null"]},"runningStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","triggerCount","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","commandSupported","imagePathPresent","triggerCount","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"imagePathPresent":{"type":"boolean"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"process":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"readinessProbeOk":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"LocalWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"WorkerHeartbeatData"},"resourceType":{"type":"string","enum":["worker"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","containerId","schedulingMode","replicas","attentionCount","replicaUnits","events"],"properties":{"attentionCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":"string"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"latestUpdateTimestamp":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"observedImage":{"type":["string","null"]},"replicaUnits":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"schedulingMode":{"type":"string","enum":["replicated","stateful","daemon"],"x-readme-ref-name":"HorizonWorkloadSchedulingMode"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"HorizonContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["horizonPlatform"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","portCount","bindMountCount","runtimeReachable","events"],"properties":{"bindMountCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":["string","null"]},"containerUnit":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"localUrl":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":["string","null"]},"portCount":{"type":"integer","format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeReachable":{"type":"boolean"},"runtimeStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ContainerHeartbeatData"},"resourceType":{"type":"string","enum":["container"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"GcpDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AzureDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"MachinesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","replicas","commandSupported","pods","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]}},"x-readme-ref-name":"KubernetesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","runtimeId","commandSupported","imagePathPresent","events"],"properties":{"commandSupported":{"type":"boolean"},"daemonInstance":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"daemonName":{"type":"string"},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"exitReason":{"type":["string","null"]},"imagePathPresent":{"type":"boolean"},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeId":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"DaemonHeartbeatData"},"resourceType":{"type":"string","enum":["daemon"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AwsComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"GcpComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AzureComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","machines"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"machines":{"type":"array","items":{"type":"object","required":["machineId","status","capacityGroup","zone","lastHeartbeat","replicaCount","drainForce"],"properties":{"capacityGroup":{"type":"string"},"cpuCores":{"type":["number","null"],"format":"double"},"drainBlockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainForce":{"type":"boolean"},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"horizondVersion":{"type":["string","null"]},"lastHeartbeat":{"type":"string"},"machineId":{"type":"string"},"memoryBytes":{"type":["integer","null"],"format":"int64"},"overlayIp":{"type":["string","null"]},"publicIp":{"type":["string","null"]},"replicaCount":{"type":"integer","format":"int64"},"status":{"type":"string"},"zone":{"type":"string"}},"x-readme-ref-name":"MachinesComputeMachineStatus"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"MachinesComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","dockerAvailable","networkAvailable"],"properties":{"dockerApiVersion":{"type":["string","null"]},"dockerArch":{"type":["string","null"]},"dockerAvailable":{"type":"boolean"},"dockerOs":{"type":["string","null"]},"dockerVersion":{"type":["string","null"]},"hostIdentifier":{"type":["string","null"]},"name":{"type":"string"},"networkAvailable":{"type":"boolean"},"networkName":{"type":["string","null"]},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"runningContainers":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"},"trackedContainers":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"LocalComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ComputeClusterHeartbeatData"},"resourceType":{"type":"string","enum":["compute-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","nodeCounts","podCounts","name","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":["string","null"]},"nodeCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"nodeStatuses":{"type":"array","items":{"type":"object","required":["name","ready","roles","labels","allocatable","capacity"],"properties":{"allocatable":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"capacity":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesNodeConditionStatus"}},"containerRuntimeVersion":{"type":["string","null"]},"kubeletVersion":{"type":["string","null"]},"labels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"ready":{"type":"boolean"},"roles":{"type":"array","items":{"type":"string"}},"uid":{"type":["string","null"]},"usage":{"oneOf":[{"type":"null"},{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"KubernetesNodeUsage"}]}},"x-readme-ref-name":"KubernetesClusterNodeStatus"}},"podCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesClusterHeartbeatData"},"resourceType":{"type":"string","enum":["kubernetes-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","approximateCounts"],"properties":{"approximateCounts":{"type":"boolean"},"approximateDelayedMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateInFlightMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateVisibleMessages":{"type":["integer","null"],"format":"int64","minimum":0},"contentBasedDeduplication":{"type":["boolean","null"]},"deduplicationScope":{"type":["string","null"]},"delaySeconds":{"type":["integer","null"],"format":"int32","minimum":0},"fifoQueue":{"type":["boolean","null"]},"fifoThroughputLimit":{"type":["string","null"]},"kmsDataKeyReusePeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"kmsMasterKeyId":{"type":["string","null"]},"maximumMessageSize":{"type":["integer","null"],"format":"int32","minimum":0},"messageRetentionPeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"queueArn":{"type":["string","null"]},"queueUrl":{"type":["string","null"]},"receiveMessageWaitTimeSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"redriveAllowPolicy":{"type":["string","null"]},"redrivePolicy":{"type":["string","null"]},"region":{"type":["string","null"]},"sqsManagedSseEnabled":{"type":["boolean","null"]},"sseEnabled":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"visibilityTimeoutSeconds":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"AwsSqsQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsSqs"]}}}]},{"allOf":[{"type":"object","required":["status","topicName","topicLabels","subscriptionLabels","messageStorageAllowedPersistenceRegions","subscriptionPushAttributes"],"properties":{"endpoint":{"type":["string","null"]},"kmsKeyName":{"type":["string","null"]},"messageStorageAllowedPersistenceRegions":{"type":"array","items":{"type":"string"}},"messageStorageEnforceInTransit":{"type":["boolean","null"]},"projectId":{"type":["string","null"]},"schemaEncoding":{"type":["string","null"]},"schemaFirstRevisionId":{"type":["string","null"]},"schemaLastRevisionId":{"type":["string","null"]},"schemaName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"subscriptionAckDeadlineSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterMaxDeliveryAttempts":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterTopic":{"type":["string","null"]},"subscriptionDetached":{"type":["boolean","null"]},"subscriptionEnableMessageOrdering":{"type":["boolean","null"]},"subscriptionFilter":{"type":["string","null"]},"subscriptionFullName":{"type":["string","null"]},"subscriptionLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionMessageRetentionDuration":{"type":["string","null"]},"subscriptionName":{"type":["string","null"]},"subscriptionPushAttributes":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionPushConfigPresent":{"type":["boolean","null"]},"subscriptionPushEndpoint":{"type":["string","null"]},"subscriptionPushNoWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionPushOidcAudience":{"type":["string","null"]},"subscriptionPushOidcServiceAccountEmail":{"type":["string","null"]},"subscriptionPushPubsubWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionRetainAckedMessages":{"type":["boolean","null"]},"subscriptionState":{"type":["string","null"]},"topicFullName":{"type":["string","null"]},"topicLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"topicMessageRetentionDuration":{"type":["string","null"]},"topicName":{"type":"string"},"topicState":{"type":["string","null"]}},"x-readme-ref-name":"GcpPubSubQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpPubSub"]}}}]},{"allOf":[{"type":"object","required":["status","name","namespaceName"],"properties":{"accessedAt":{"type":["string","null"]},"activeMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"autoDeleteOnIdle":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"deadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"deadLetteringOnMessageExpiration":{"type":["boolean","null"]},"defaultMessageTimeToLive":{"type":["string","null"]},"duplicateDetectionHistoryTimeWindow":{"type":["string","null"]},"enableBatchedOperations":{"type":["boolean","null"]},"enableExpress":{"type":["boolean","null"]},"enablePartitioning":{"type":["boolean","null"]},"endpoint":{"type":["string","null"]},"forwardDeadLetteredMessagesTo":{"type":["string","null"]},"forwardTo":{"type":["string","null"]},"lockDuration":{"type":["string","null"]},"maxDeliveryCount":{"type":["integer","null"],"format":"int32","minimum":0},"maxMessageSizeInKilobytes":{"type":["integer","null"],"format":"int64","minimum":0},"maxSizeInMegabytes":{"type":["integer","null"],"format":"int32","minimum":0},"messageCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"namespaceName":{"type":"string"},"queueStatus":{"type":["string","null"]},"requiresDuplicateDetection":{"type":["boolean","null"]},"requiresSession":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"scheduledMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"sizeInBytes":{"type":["integer","null"],"format":"int64","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"transferDeadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"transferMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"updatedAt":{"type":["string","null"]}},"x-readme-ref-name":"AzureServiceBusQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureServiceBus"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"name":{"type":"string"},"path":{"type":["string","null"]},"serviceStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"}},"x-readme-ref-name":"LocalQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"QueueHeartbeatData"},"resourceType":{"type":"string","enum":["queue"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","keySchema"],"properties":{"billingMode":{"type":["string","null"]},"deletionProtectionEnabled":{"type":["boolean","null"]},"globalSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"itemCount":{"type":["integer","null"],"format":"int64","minimum":0},"keySchema":{"type":"array","items":{"type":"object","required":["attributeName","keyType"],"properties":{"attributeName":{"type":"string"},"keyType":{"type":"string"}},"x-readme-ref-name":"AwsDynamoDbKeySchemaElement"}},"localSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"region":{"type":["string","null"]},"replicaCount":{"type":["integer","null"],"format":"int32","minimum":0},"restoreInProgress":{"type":["boolean","null"]},"sseStatus":{"type":["string","null"]},"sseType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"streamEnabled":{"type":["boolean","null"]},"streamViewType":{"type":["string","null"]},"tableArn":{"type":["string","null"]},"tableClass":{"type":["string","null"]},"tableSizeBytes":{"type":["integer","null"],"format":"int64","minimum":0},"tableStatus":{"type":["string","null"]},"ttlAttributeName":{"type":["string","null"]},"ttlStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsDynamoDbKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsDynamoDb"]}}}]},{"allOf":[{"type":"object","required":["status","databaseName","cmekEnabled","sourceInfoPresent"],"properties":{"appEngineIntegrationMode":{"type":["string","null"]},"cmekEnabled":{"type":"boolean"},"concurrencyMode":{"type":["string","null"]},"createTime":{"type":["string","null"]},"databaseEdition":{"type":["string","null"]},"databaseName":{"type":"string"},"databaseType":{"type":["string","null"]},"deleteProtectionState":{"type":["string","null"]},"deleteTime":{"type":["string","null"]},"earliestVersionTime":{"type":["string","null"]},"endpoint":{"type":["string","null"]},"locationId":{"type":["string","null"]},"pointInTimeRecoveryEnablement":{"type":["string","null"]},"projectId":{"type":["string","null"]},"sourceInfoPresent":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"updateTime":{"type":["string","null"]},"versionRetentionPeriod":{"type":["string","null"]}},"x-readme-ref-name":"GcpFirestoreKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpFirestore"]}}}]},{"allOf":[{"type":"object","required":["status","tableName","storageAccountName","tableExists"],"properties":{"endpoint":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"signedIdentifierCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"storageAccountKind":{"type":["string","null"]},"storageAccountLocation":{"type":["string","null"]},"storageAccountName":{"type":"string"},"storageAccountPrimaryStatus":{"type":["string","null"]},"storageAccountProvisioningState":{"type":["string","null"]},"storageAccountResourceId":{"type":["string","null"]},"tableExists":{"type":"boolean"},"tableName":{"type":"string"}},"x-readme-ref-name":"AzureTableKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureTable"]}}}]},{"allOf":[{"type":"object","required":["status","name","path","pathExists","cloudMetadataSupported"],"properties":{"cloudMetadataSupported":{"type":"boolean"},"isDirectory":{"type":["boolean","null"]},"name":{"type":"string"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"}},"x-readme-ref-name":"LocalKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"KvHeartbeatData"},"resourceType":{"type":"string","enum":["kv"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"description":"AWS Aurora Serverless v2 backend.","type":"object","required":["status","clusterIdentifier","neverPauses"],"properties":{"clusterIdentifier":{"type":"string"},"endpoint":{"type":["string","null"]},"engineVersion":{"type":["string","null"]},"neverPauses":{"type":"boolean","description":"True when a `minCapacity: 0` instance has not reached 0 ACU over the observation\nwindow — it is silently paying always-on prices (auto-pause verification)."},"serverlessCapacity":{"type":["number","null"],"format":"double","description":"Latest sampled `ServerlessDatabaseCapacity` (ACU)."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"AuroraPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aurora"]}}}],"description":"AWS Aurora Serverless v2 backend."},{"allOf":[{"description":"GCP Cloud SQL backend.","type":"object","required":["status","instanceName"],"properties":{"databaseVersion":{"type":["string","null"]},"instanceName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudSqlPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["cloudSql"]}}}],"description":"GCP Cloud SQL backend."},{"allOf":[{"description":"Azure Flexible Server backend.","type":"object","required":["status","serverName"],"properties":{"serverName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"AzureFlexibleServerPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["flexibleServer"]}}}],"description":"Azure Flexible Server backend."},{"allOf":[{"description":"Local embedded Postgres backend.","type":"object","required":["status","name","version","processRunning"],"properties":{"name":{"type":"string"},"port":{"type":["integer","null"],"format":"int32","minimum":0},"processRunning":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":"string"}},"x-readme-ref-name":"LocalPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}],"description":"Local embedded Postgres backend."}],"x-readme-ref-name":"PostgresHeartbeatData"},"resourceType":{"type":"string","enum":["postgres"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","accountId","region","prefix","parameterMetadataSampled"],"properties":{"accountId":{"type":"string"},"hasMoreParameters":{"type":["boolean","null"]},"latestModifiedAt":{"type":["string","null"],"format":"date-time"},"parameterMetadataSampled":{"type":"boolean"},"prefix":{"type":"string"},"region":{"type":"string"},"sampledAdvancedTierCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledKmsKeyMetadataPresentCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledParameterCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledSecureStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringListCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"AwsParameterStoreVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsParameterStore"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","prefix","secretMetadataListed"],"properties":{"location":{"type":"string"},"prefix":{"type":"string"},"projectId":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"GcpSecretManagerVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpSecretManager"]}}}]},{"allOf":[{"type":"object","required":["status","name","softDeleteEnabled","softDeleteRetentionDays","rbacAuthorizationEnabled","publicNetworkAccess","accessPolicyCount","privateEndpointConnectionCount","secretMetadataListed"],"properties":{"accessPolicyCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":["string","null"]},"name":{"type":"string"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"purgeProtectionEnabled":{"type":["boolean","null"]},"rbacAuthorizationEnabled":{"type":"boolean"},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secretMetadataListed":{"type":"boolean"},"skuFamily":{"type":["string","null"]},"skuName":{"type":["string","null"]},"softDeleteEnabled":{"type":"boolean"},"softDeleteRetentionDays":{"type":"integer","format":"int32"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"},"vaultUri":{"type":["string","null"]}},"x-readme-ref-name":"AzureKeyVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureKeyVault"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","prefix","secretMetadataListed"],"properties":{"namespace":{"type":"string"},"prefix":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"KubernetesSecretVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesSecret"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists","secretMetadataListed"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"LocalVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"VaultHeartbeatData"},"resourceType":{"type":"string","enum":["vault"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","roleName","roleArn","roleId","path","createDate","assumeRolePolicyPresent","tagCount","managedTagCount","attachedPolicyCount","attachedPolicyNames","inlinePolicyCount","inlinePolicyNames","stackPermissionsApplied"],"properties":{"assumeRolePolicyPresent":{"type":"boolean"},"attachedPolicyCount":{"type":"integer","format":"int32","minimum":0},"attachedPolicyNames":{"type":"array","items":{"type":"string"}},"createDate":{"type":"string"},"description":{"type":["string","null"]},"inlinePolicyCount":{"type":"integer","format":"int32","minimum":0},"inlinePolicyNames":{"type":"array","items":{"type":"string"}},"lastUsedDate":{"type":["string","null"]},"lastUsedRegion":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"maxSessionDuration":{"type":["integer","null"],"format":"int32"},"path":{"type":"string"},"permissionsBoundaryArn":{"type":["string","null"]},"permissionsBoundaryType":{"type":["string","null"]},"roleArn":{"type":"string"},"roleId":{"type":"string"},"roleName":{"type":"string"},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tagCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsIamRoleServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","email","projectBindingCount","projectRoles","serviceAccountBindingCount","serviceAccountRoles"],"properties":{"description":{"type":["string","null"]},"disabled":{"type":["boolean","null"]},"displayName":{"type":["string","null"]},"email":{"type":"string"},"etag":{"type":["string","null"]},"name":{"type":["string","null"]},"oauth2ClientId":{"type":["string","null"]},"projectBindingCount":{"type":"integer","format":"int32","minimum":0},"projectId":{"type":["string","null"]},"projectRoles":{"type":"array","items":{"type":"string"}},"serviceAccountBindingCount":{"type":"integer","format":"int32","minimum":0},"serviceAccountRoles":{"type":"array","items":{"type":"string"}},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"uniqueId":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceId","resourceGroup","location","managedTagCount","roleAssignmentCount","roleAssignmentIds","customRoleDefinitionCount","customRoleDefinitionIds","stackPermissionsApplied"],"properties":{"clientId":{"type":["string","null"]},"customRoleDefinitionCount":{"type":"integer","format":"int32","minimum":0},"customRoleDefinitionIds":{"type":"array","items":{"type":"string"}},"isolationScope":{"type":["string","null"]},"location":{"type":"string"},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"principalId":{"type":["string","null"]},"resourceGroup":{"type":"string"},"resourceId":{"type":"string"},"roleAssignmentCount":{"type":"integer","format":"int32","minimum":0},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tenantId":{"type":["string","null"]},"type":{"type":["string","null"]}},"x-readme-ref-name":"AzureManagedIdentityServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]},{"allOf":[{"type":"object","required":["status","identity","configured"],"properties":{"configured":{"type":"boolean"},"identity":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"}},"x-readme-ref-name":"LocalServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ServiceAccountHeartbeatData"},"resourceType":{"type":"string","enum":["service-account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","publicSubnetIds","privateSubnetIds","availabilityZones","routeTableCount","isByoVpc"],"properties":{"availabilityZones":{"type":"array","items":{"type":"string"}},"cidrBlock":{"type":["string","null"]},"internetGatewayId":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"natGatewayId":{"type":["string","null"]},"privateSubnetIds":{"type":"array","items":{"type":"string"}},"publicSubnetIds":{"type":"array","items":{"type":"string"}},"routeTableCount":{"type":"integer","format":"int32","minimum":0},"securityGroupId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vpcId":{"type":["string","null"]},"vpcState":{"type":["string","null"]}},"x-readme-ref-name":"AwsVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVpc"],"properties":{"cidrBlock":{"type":["string","null"]},"cloudNatName":{"type":["string","null"]},"firewallName":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"networkName":{"type":["string","null"]},"networkSelfLink":{"type":["string","null"]},"region":{"type":["string","null"]},"routerName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"subnetworkName":{"type":["string","null"]},"subnetworkSelfLink":{"type":["string","null"]}},"x-readme-ref-name":"GcpVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVnet"],"properties":{"applicationGatewaySubnetName":{"type":["string","null"]},"cidrBlock":{"type":["string","null"]},"isByoVnet":{"type":"boolean"},"lastByoVnetVerificationErrorCode":{"type":["string","null"]},"location":{"type":["string","null"]},"natGatewayId":{"type":["string","null"]},"nsgId":{"type":["string","null"]},"privateEndpointSubnetName":{"type":["string","null"]},"privateSubnetName":{"type":["string","null"]},"publicIpId":{"type":["string","null"]},"publicSubnetName":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vnetName":{"type":["string","null"]},"vnetResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureVnetNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureVnet"]}}}]}],"x-readme-ref-name":"NetworkHeartbeatData"},"resourceType":{"type":"string","enum":["network"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","managementPermissionsApplied"],"properties":{"managementPermissionsApplied":{"type":"boolean"},"roleArn":{"type":["string","null"]},"roleName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"AwsRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","roleBound","impersonationGranted"],"properties":{"impersonationGranted":{"type":"boolean"},"roleBound":{"type":"boolean"},"serviceAccountEmail":{"type":["string","null"]},"serviceAccountUniqueId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"GcpRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","roleAssignmentIds"],"properties":{"ficName":{"type":["string","null"]},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"roleDefinitionId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"},"tenantId":{"type":["string","null"]},"uamiClientId":{"type":["string","null"]},"uamiPrincipalId":{"type":["string","null"]},"uamiResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]}],"x-readme-ref-name":"RemoteStackManagementHeartbeatData"},"resourceType":{"type":"string","enum":["remote-stack-management"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","registryId","region","registryUri","repositoryPrefix","repositoryCount","repositoriesTruncated","repositories"],"properties":{"pullRoleArn":{"type":["string","null"]},"pushRoleArn":{"type":["string","null"]},"region":{"type":"string"},"registryId":{"type":"string"},"registryUri":{"type":"string"},"repositories":{"type":"array","items":{"type":"object","required":["repositoryArn","registryId","repositoryName","repositoryUri","createdAt","kmsKeyPresent"],"properties":{"createdAt":{"type":"number","format":"double"},"encryptionType":{"type":["string","null"]},"imageTagMutability":{"type":["string","null"]},"kmsKeyPresent":{"type":"boolean"},"registryId":{"type":"string"},"repositoryArn":{"type":"string"},"repositoryName":{"type":"string"},"repositoryUri":{"type":"string"},"scanOnPush":{"type":["boolean","null"]}},"x-readme-ref-name":"AwsEcrRepositoryHeartbeatData"}},"repositoriesTruncated":{"type":"boolean"},"repositoryCount":{"type":"integer","format":"int32","minimum":0},"repositoryPrefix":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"AwsEcrArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsEcr"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","repositoryId","labelCount","cleanupPolicyCount","kmsKeyNamePresent","iamPolicyEtagPresent","iamBindingCount","iamRoles"],"properties":{"cleanupPolicyCount":{"type":"integer","format":"int32","minimum":0},"cleanupPolicyDryRun":{"type":["boolean","null"]},"createTime":{"type":["string","null"]},"description":{"type":["string","null"]},"format":{"type":["string","null"]},"iamBindingCount":{"type":"integer","format":"int32","minimum":0},"iamPolicyEtagPresent":{"type":"boolean"},"iamRoles":{"type":"array","items":{"type":"string"}},"kmsKeyNamePresent":{"type":"boolean"},"labelCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"mode":{"type":["string","null"]},"name":{"type":["string","null"]},"projectId":{"type":"string"},"pullServiceAccountEmail":{"type":["string","null"]},"pushServiceAccountEmail":{"type":["string","null"]},"repositoryId":{"type":"string"},"satisfiesPzs":{"type":["boolean","null"]},"sizeBytes":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"updateTime":{"type":["string","null"]}},"x-readme-ref-name":"GcpArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpArtifactRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceGroup","location","skuName","adminUserEnabled","anonymousPullEnabled","publicNetworkAccess","networkRuleBypassOptions","ipRuleCount","encryptionKeyVaultUriPresent","encryptionKeyIdentifierPresent","policiesPresent","policyCount","privateEndpointConnectionCount","dataEndpointHostNames","zoneRedundancy","managedTagCount"],"properties":{"adminUserEnabled":{"type":"boolean"},"anonymousPullEnabled":{"type":"boolean"},"creationDate":{"type":["string","null"]},"dataEndpointEnabled":{"type":["boolean","null"]},"dataEndpointHostNames":{"type":"array","items":{"type":"string"}},"encryptionKeyIdentifierPresent":{"type":"boolean"},"encryptionKeyVaultUriPresent":{"type":"boolean"},"encryptionStatus":{"type":["string","null"]},"ipRuleCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"loginServer":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"networkRuleBypassOptions":{"type":"string"},"networkRuleDefaultAction":{"type":["string","null"]},"policiesPresent":{"type":"boolean"},"policyCount":{"type":"integer","format":"int32","minimum":0},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"resourceGroup":{"type":"string"},"resourceId":{"type":["string","null"]},"skuName":{"type":"string"},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"type":{"type":["string","null"]},"zoneRedundancy":{"type":"string"}},"x-readme-ref-name":"AzureContainerRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","registryUrl","reachable"],"properties":{"reachable":{"type":"boolean"},"registryUrl":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"LocalArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ArtifactRegistryHeartbeatData"},"resourceType":{"type":"string","enum":["artifact-registry"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectName","environmentVariableCount","serviceRolePresent","encryptionKeyPresent"],"properties":{"artifactsEncryptionDisabled":{"type":["boolean","null"]},"artifactsType":{"type":["string","null"]},"cloudWatchLogsStatus":{"type":["string","null"]},"computeType":{"type":["string","null"]},"created":{"type":["number","null"],"format":"double"},"description":{"type":["string","null"]},"encryptionKeyPresent":{"type":"boolean"},"environmentImage":{"type":["string","null"]},"environmentType":{"type":["string","null"]},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"imagePullCredentialsType":{"type":["string","null"]},"lastModified":{"type":["number","null"],"format":"double"},"privilegedMode":{"type":["boolean","null"]},"projectArn":{"type":["string","null"]},"projectName":{"type":"string"},"queuedTimeoutInMinutes":{"type":["integer","null"],"format":"int32"},"s3LogsStatus":{"type":["string","null"]},"serviceRolePresent":{"type":"boolean"},"sourceType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"timeoutInMinutes":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"AwsCodeBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsCodeBuild"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","buildConfigId","environmentVariableCount"],"properties":{"buildConfigId":{"type":"string"},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"projectId":{"type":"string"},"serviceAccount":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudBuild"]}}}]},{"allOf":[{"type":"object","required":["status","managedEnvironmentId","resourceGroupName","environmentVariableCount"],"properties":{"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"managedEnvironmentId":{"type":"string"},"managedIdentityId":{"type":["string","null"]},"resourceGroupName":{"type":"string"},"resourcePrefix":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","jobName","namespace","conditionCount","events"],"properties":{"active":{"type":["integer","null"],"format":"int32"},"completionTime":{"type":["string","null"],"format":"date-time"},"conditionCount":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"failed":{"type":["integer","null"],"format":"int32"},"imageDigest":{"type":["string","null"]},"jobName":{"type":"string"},"namespace":{"type":"string"},"startTime":{"type":["string","null"],"format":"date-time"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"succeeded":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"KubernetesBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesJob"]}}}]}],"x-readme-ref-name":"BuildHeartbeatData"},"resourceType":{"type":"string","enum":["build"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectId","serviceName","enabled"],"properties":{"enabled":{"type":"boolean"},"lastOperationName":{"type":["string","null"]},"projectId":{"type":"string"},"serviceName":{"type":"string"},"serviceResourceName":{"type":["string","null"]},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"},"title":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceUsageActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceUsage"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","resourceTypeCount","registered"],"properties":{"namespace":{"type":"string"},"providerId":{"type":["string","null"]},"registered":{"type":"boolean"},"registrationPolicy":{"type":["string","null"]},"registrationState":{"type":["string","null"]},"resourceTypeCount":{"type":"integer","format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceProviderActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureResourceProvider"]}}}]}],"x-readme-ref-name":"ServiceActivationHeartbeatData"},"resourceType":{"type":"string","enum":["service_activation"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","managedTags"],"properties":{"location":{"type":["string","null"]},"managedTags":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatData"},"resourceType":{"type":"string","enum":["azure_resource_group"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","primaryEndpoints","secondaryEndpoints"],"properties":{"allowBlobPublicAccess":{"type":["boolean","null"]},"allowSharedKeyAccess":{"type":["boolean","null"]},"encryptionKeySource":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"networkBypass":{"type":["string","null"]},"networkDefaultAction":{"type":["string","null"]},"networkIpRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkResourceAccessRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkVirtualNetworkRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"primaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"requireInfrastructureEncryption":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secondaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"supportsHttpsTrafficOnly":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureStorageAccountHeartbeatData"},"resourceType":{"type":"string","enum":["azure_storage_account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","workloadProfileCount","workloadProfiles"],"properties":{"customDomainVerificationId":{"type":["string","null"]},"defaultDomain":{"type":["string","null"]},"eventStreamEndpoint":{"type":["string","null"]},"infrastructureResourceGroup":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"staticIp":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatStatus"},"workloadProfileCount":{"type":"integer","format":"int32","minimum":0},"workloadProfiles":{"type":"array","items":{"type":"object","required":["name","workloadProfileType"],"properties":{"maximumCount":{"type":["integer","null"],"format":"int32"},"minimumCount":{"type":["integer","null"],"format":"int32"},"name":{"type":"string"},"workloadProfileType":{"type":"string"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentWorkloadProfile"}},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatData"},"resourceType":{"type":"string","enum":["azure_container_apps_environment"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","privateEndpointConnectionCount"],"properties":{"createdAt":{"type":["string","null"]},"disableLocalAuth":{"type":["boolean","null"]},"location":{"type":["string","null"]},"metricId":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"namespaceStatus":{"type":["string","null"]},"premiumMessagingPartitions":{"type":["integer","null"],"format":"int32"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"serviceBusEndpoint":{"type":["string","null"]},"skuCapacity":{"type":["integer","null"],"format":"int32"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"updatedAt":{"type":["string","null"]},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureServiceBusNamespaceHeartbeatData"},"resourceType":{"type":"string","enum":["azure_service_bus_namespace"]}}}],"x-readme-ref-name":"ResourceHeartbeatData"} \ No newline at end of file +{"oneOf":[{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent","publicAccessBlockPresent"],"properties":{"blockPublicAcls":{"type":["boolean","null"]},"blockPublicPolicy":{"type":["boolean","null"]},"bucketAclPresent":{"type":["boolean","null"]},"bucketLocation":{"type":["string","null"]},"bucketPolicyPresent":{"type":["boolean","null"]},"encryptionConfigPresent":{"type":"boolean"},"encryptionEnabled":{"type":["boolean","null"]},"ignorePublicAcls":{"type":["boolean","null"]},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"publicAccessBlockPresent":{"type":"boolean"},"region":{"type":["string","null"]},"restrictPublicBuckets":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"versioningEnabled":{"type":["boolean","null"]},"versioningStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsS3StorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsS3"]}}}]},{"allOf":[{"type":"object","required":["status","name","lifecyclePresent","encryptionConfigPresent"],"properties":{"bucketId":{"type":["string","null"]},"defaultKmsKeyName":{"type":["string","null"]},"encryptionConfigPresent":{"type":"boolean"},"lifecyclePresent":{"type":"boolean"},"lifecycleRuleCount":{"type":["integer","null"],"format":"int64","minimum":0},"location":{"type":["string","null"]},"locationType":{"type":["string","null"]},"name":{"type":"string"},"publicAccessPrevention":{"type":["string","null"]},"retentionPeriod":{"type":["string","null"]},"retentionPolicyEffectiveTime":{"type":["string","null"]},"retentionPolicyIsLocked":{"type":["boolean","null"]},"softDeleteEffectiveTime":{"type":["string","null"]},"softDeleteRetentionDurationSeconds":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"storageClass":{"type":["string","null"]},"uniformBucketLevelAccessEnabled":{"type":["boolean","null"]},"uniformBucketLevelAccessLockedTime":{"type":["string","null"]},"versioningEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"GcpCloudStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudStorage"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"accessTier":{"type":["string","null"]},"accountKind":{"type":["string","null"]},"allowBlobPublicAccess":{"type":["boolean","null"]},"blobDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"blobDeleteRetentionEnabled":{"type":["boolean","null"]},"blobEncryptionEnabled":{"type":["boolean","null"]},"blobVersioningEnabled":{"type":["boolean","null"]},"changeFeedEnabled":{"type":["boolean","null"]},"changeFeedRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionDays":{"type":["integer","null"],"format":"int64","minimum":0},"containerDeleteRetentionEnabled":{"type":["boolean","null"]},"containerPublicAccess":{"type":["string","null"]},"encryptionKeySource":{"type":["string","null"]},"fileEncryptionEnabled":{"type":["boolean","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"primaryLocation":{"type":["string","null"]},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"queueEncryptionEnabled":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"secondaryLocation":{"type":["string","null"]},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"statusOfPrimary":{"type":["string","null"]},"statusOfSecondary":{"type":["string","null"]},"storageAccountName":{"type":["string","null"]},"tableEncryptionEnabled":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureBlobStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureBlob"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"}},"x-readme-ref-name":"LocalStorageHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"StorageHeartbeatData"},"resourceType":{"type":"string","enum":["storage"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","functionName","layerCount","functionUrlCorsPresent","triggerCount"],"properties":{"codeSha256":{"type":["string","null"]},"functionName":{"type":"string"},"functionUrlAuthType":{"type":["string","null"]},"functionUrlCorsPresent":{"type":"boolean"},"lastModified":{"type":["string","null"]},"lastUpdateStatus":{"type":["string","null"]},"lastUpdateStatusReason":{"type":["string","null"]},"lastUpdateStatusReasonCode":{"type":["string","null"]},"layerCount":{"type":"integer","format":"int32","minimum":0},"memorySizeMb":{"type":["integer","null"],"format":"int64"},"packageType":{"type":["string","null"]},"revisionId":{"type":["string","null"]},"runtime":{"type":["string","null"]},"state":{"type":["string","null"]},"stateReason":{"type":["string","null"]},"stateReasonCode":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"timeoutSeconds":{"type":["integer","null"],"format":"int64"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"version":{"type":["string","null"]}},"x-readme-ref-name":"AwsLambdaWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsLambda"]}}}]},{"allOf":[{"type":"object","required":["status","service","urls","trafficCount"],"properties":{"containerImage":{"type":["string","null"]},"cpuLimit":{"type":["string","null"]},"generation":{"type":["integer","null"],"format":"int64"},"latestCreatedRevision":{"type":["string","null"]},"latestReadyRevision":{"type":["string","null"]},"maxInstanceCount":{"type":["integer","null"],"format":"int32"},"memoryLimit":{"type":["string","null"]},"minInstanceCount":{"type":["integer","null"],"format":"int32"},"observedGeneration":{"type":["integer","null"],"format":"int64"},"region":{"type":["string","null"]},"service":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"trafficCount":{"type":"integer","format":"int32","minimum":0},"uri":{"type":["string","null"]},"urls":{"type":"array","items":{"type":"string"}}},"x-readme-ref-name":"GcpCloudRunWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudRun"]}}}]},{"allOf":[{"type":"object","required":["status","appName"],"properties":{"appName":{"type":"string"},"cpu":{"type":["number","null"],"format":"double"},"environmentName":{"type":["string","null"]},"ingressFqdn":{"type":["string","null"]},"maxReplicas":{"type":["integer","null"],"format":"int32"},"memory":{"type":["string","null"]},"minReplicas":{"type":["integer","null"],"format":"int32"},"provisioningState":{"type":["string","null"]},"revision":{"type":["string","null"]},"runningStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","triggerCount","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","commandSupported","imagePathPresent","triggerCount","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"imagePathPresent":{"type":"boolean"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"process":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"readinessProbeOk":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"triggerCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"LocalWorkerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"WorkerHeartbeatData"},"resourceType":{"type":"string","enum":["worker"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","containerId","schedulingMode","replicas","attentionCount","replicaUnits","events"],"properties":{"attentionCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":"string"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"latestUpdateTimestamp":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"observedImage":{"type":["string","null"]},"replicaUnits":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"schedulingMode":{"type":"string","enum":["replicated","stateful","daemon"],"x-readme-ref-name":"HorizonWorkloadSchedulingMode"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"HorizonContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["horizonPlatform"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","workloadKind","replicas","pods","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]},"workloadKind":{"type":"string","enum":["deployment","statefulSet","daemonSet","replicaSet","pod"],"x-readme-ref-name":"KubernetesWorkloadKind"}},"x-readme-ref-name":"KubernetesContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","portCount","bindMountCount","runtimeReachable","events"],"properties":{"bindMountCount":{"type":"integer","format":"int32","minimum":0},"containerId":{"type":["string","null"]},"containerUnit":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"image":{"type":["string","null"]},"localUrl":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":["string","null"]},"portCount":{"type":"integer","format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeReachable":{"type":"boolean"},"runtimeStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalContainerHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ContainerHeartbeatData"},"resourceType":{"type":"string","enum":["container"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"GcpDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AzureDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","horizonClusterId","horizonStatus","capacityGroup","desiredMachines","assignedMachines","healthyInstances","unavailableInstances","commandSupported","latestUpdateTimestamp","daemonInstances","events"],"properties":{"assignedMachines":{"type":"integer","format":"int32","minimum":0},"capacityGroup":{"type":"string"},"commandSupported":{"type":"boolean"},"daemonInstances":{"type":"array","items":{"type":"object","required":["replicaId","name","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"ip":{"type":["string","null"]},"machineId":{"type":["string","null"]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"message":{"type":["string","null"]},"metricsHealthy":{"type":["boolean","null"]},"metricsLastUpdated":{"type":["string","null"]},"metricsStatus":{"type":["string","null"]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"phase":{"type":["string","null"]},"ready":{"type":"boolean"},"reason":{"type":["string","null"]},"replicaId":{"type":"string"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":["string","null"]},"terminatedReason":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeUnitStatus"}},"daemonName":{"type":"string"},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"eventId":{"type":["string","null"]},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"details":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"id":{"type":["string","null"]},"kind":{"type":["string","null"]},"machineId":{"type":["string","null"]},"name":{"type":["string","null"]},"replicaId":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"ManagedRuntimeEventSnapshot"}},"healthyInstances":{"type":"integer","format":"int32","minimum":0},"horizonClusterId":{"type":"string"},"horizonStatus":{"type":"string"},"horizonStatusMessage":{"type":["string","null"]},"horizonStatusReason":{"type":["string","null"]},"latestUpdateTimestamp":{"type":"string"},"observedImage":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"unavailableInstances":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"MachinesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","name","replicas","commandSupported","pods","events"],"properties":{"commandSupported":{"type":"boolean"},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":"string"},"pods":{"type":"array","items":{"type":"object","required":["name","ready","restartCount","ownerReferences"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodeName":{"type":["string","null"]},"ownerReferences":{"type":"array","items":{"type":"object","required":["kind","name","uid","controller"],"properties":{"controller":{"type":"boolean"},"kind":{"type":"string"},"name":{"type":"string"},"uid":{"type":"string"}},"x-readme-ref-name":"KubernetesOwnerReference"}},"phase":{"type":["string","null"]},"podIp":{"type":["string","null"]},"ready":{"type":"boolean"},"restartCount":{"type":"integer","format":"int32","minimum":0},"terminatedReason":{"type":["string","null"]},"uid":{"type":["string","null"]},"waitingReason":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesPodRuntimeUnitStatus"}},"replicas":{"type":"object","properties":{"available":{"type":["integer","null"],"format":"int32","minimum":0},"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"misscheduled":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0},"updated":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"WorkloadReplicaStatus"},"restarts":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"workload":{"oneOf":[{"type":"null"},{"type":"object","required":["conditions"],"properties":{"availableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"lastTransitionTime":{"type":["string","null"],"format":"date-time"},"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesWorkloadCondition"}},"desiredGeneration":{"type":["integer","null"],"format":"int64"},"desiredReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"observedGeneration":{"type":["integer","null"],"format":"int64"},"readyReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"rolloutReason":{"type":["string","null"]},"updatedReplicas":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"KubernetesWorkloadStatus"}]}},"x-readme-ref-name":"KubernetesDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetes"]}}}]},{"allOf":[{"type":"object","required":["status","runtimeId","commandSupported","imagePathPresent","events"],"properties":{"commandSupported":{"type":"boolean"},"daemonInstance":{"oneOf":[{"type":"null"},{"type":"object","required":["unitId","name","kind","ready"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"kind":{"type":"string","enum":["container","process","daemon"],"x-readme-ref-name":"LocalRuntimeUnitKind"},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"phase":{"type":["string","null"]},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":"boolean"},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"unitId":{"type":"string"}},"x-readme-ref-name":"LocalRuntimeUnitStatus"}]},"daemonName":{"type":"string"},"events":{"type":"array","items":{"type":"object","required":["timestamp","severity","kind","message"],"properties":{"kind":{"type":"string"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"subject":{"oneOf":[{"type":"null"},{"type":"object","required":["kind"],"properties":{"id":{"type":["string","null"]},"kind":{"type":"string"},"name":{"type":["string","null"]}},"x-readme-ref-name":"LocalRuntimeEventSubject"}]},"timestamp":{"type":"string","format":"date-time"}},"x-readme-ref-name":"LocalRuntimeEventSnapshot"}},"exitReason":{"type":["string","null"]},"imagePathPresent":{"type":"boolean"},"pid":{"type":["integer","null"],"format":"int32","minimum":0},"restartCount":{"type":["integer","null"],"format":"int32","minimum":0},"runtimeId":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"}},"x-readme-ref-name":"LocalDaemonHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"DaemonHeartbeatData"},"resourceType":{"type":"string","enum":["daemon"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AwsComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aws"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"GcpComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcp"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","providerFleets"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"providerFleets":{"type":"array","items":{"type":"object","required":["groupId","providerId","currentMachines","desiredMachines"],"properties":{"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"groupId":{"type":"string"},"location":{"type":["string","null"]},"providerId":{"type":"string"}},"x-readme-ref-name":"ProviderFleetStatus"}},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"AzureComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azure"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","capacityGroups","machines"],"properties":{"backendClusterId":{"type":["string","null"]},"capacityGroups":{"type":"array","items":{"type":"object","required":["groupId","currentMachines","desiredMachines"],"properties":{"capacityBlocker":{"oneOf":[{"type":"null"},{"type":"object","required":["category","message","observedAt"],"properties":{"category":{"type":"string","enum":["quota","capacity","allocation","other"],"x-readme-ref-name":"ComputeCapacityBlockerCategory"},"message":{"type":"string"},"observedAt":{"type":"string","format":"date-time"},"providerCode":{"type":["string","null"]},"providerReference":{"type":["string","null"]}},"x-readme-ref-name":"ComputeCapacityBlocker"}]},"currentMachines":{"type":"integer","format":"int32","minimum":0},"desiredMachines":{"type":"integer","format":"int32","minimum":0},"drainProgress":{"oneOf":[{"type":"null"},{"type":"object","required":["machineId","status","replicaCount","force","stalled"],"properties":{"blockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"force":{"type":"boolean"},"machineId":{"type":"string"},"replicaCount":{"type":"integer","format":"int64"},"stalled":{"type":"boolean"},"status":{"type":"string","enum":["draining","drained","terminating"],"x-readme-ref-name":"ComputeDrainProgressStatus"}},"x-readme-ref-name":"ComputeDrainProgress"}]},"groupId":{"type":"string"},"instanceType":{"type":["string","null"]},"maxMachines":{"type":["integer","null"],"format":"int32","minimum":0},"minMachines":{"type":["integer","null"],"format":"int32","minimum":0},"recommendation":{"oneOf":[{"type":"null"},{"type":"object","required":["desiredMachines"],"properties":{"desiredMachines":{"type":"integer","format":"int32","minimum":0},"reason":{"type":["string","null"]},"unschedulableReplicas":{"type":["integer","null"],"format":"int32","minimum":0},"utilization":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"ComputeCapacityRecommendation"}]}},"x-readme-ref-name":"ComputeCapacityGroupStatus"}},"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"machines":{"type":"array","items":{"type":"object","required":["machineId","status","capacityGroup","zone","lastHeartbeat","replicaCount","drainForce"],"properties":{"capacityGroup":{"type":"string"},"cpuCores":{"type":["number","null"],"format":"double"},"drainBlockers":{"type":"array","items":{"type":"object","required":["workloadName","replicaId","schedulingMode","state","reason"],"properties":{"reason":{"type":"string"},"replicaId":{"type":"string"},"schedulingMode":{"type":"string"},"state":{"type":"string"},"workloadName":{"type":"string"}},"x-readme-ref-name":"ComputeDrainBlocker"}},"drainDeadlineAt":{"type":["string","null"]},"drainForce":{"type":"boolean"},"drainRequestedAt":{"type":["string","null"]},"drainedAt":{"type":["string","null"]},"horizondVersion":{"type":["string","null"]},"lastHeartbeat":{"type":"string"},"machineId":{"type":"string"},"memoryBytes":{"type":["integer","null"],"format":"int64"},"overlayIp":{"type":["string","null"]},"publicIp":{"type":["string","null"]},"replicaCount":{"type":"integer","format":"int64"},"status":{"type":"string"},"zone":{"type":"string"}},"x-readme-ref-name":"MachinesComputeMachineStatus"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"}},"x-readme-ref-name":"MachinesComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["machines"]}}}]},{"allOf":[{"type":"object","required":["status","nodes","name","dockerAvailable","networkAvailable"],"properties":{"dockerApiVersion":{"type":["string","null"]},"dockerArch":{"type":["string","null"]},"dockerAvailable":{"type":"boolean"},"dockerOs":{"type":["string","null"]},"dockerVersion":{"type":["string","null"]},"hostIdentifier":{"type":["string","null"]},"name":{"type":"string"},"networkAvailable":{"type":"boolean"},"networkName":{"type":["string","null"]},"nodes":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"runningContainers":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ComputeClusterHeartbeatStatus"},"trackedContainers":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"LocalComputeClusterHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ComputeClusterHeartbeatData"},"resourceType":{"type":"string","enum":["compute-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","nodeCounts","podCounts","name","events"],"properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"name":{"type":"string"},"namespace":{"type":["string","null"]},"nodeCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"nodeStatuses":{"type":"array","items":{"type":"object","required":["name","ready","roles","labels","allocatable","capacity"],"properties":{"allocatable":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"capacity":{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"pods":{"type":["integer","null"],"format":"int64","minimum":0}},"x-readme-ref-name":"KubernetesNodeResources"},"conditions":{"type":"array","items":{"type":"object","required":["type","status"],"properties":{"message":{"type":["string","null"]},"reason":{"type":["string","null"]},"status":{"type":"string"},"type":{"type":"string"}},"x-readme-ref-name":"KubernetesNodeConditionStatus"}},"containerRuntimeVersion":{"type":["string","null"]},"kubeletVersion":{"type":["string","null"]},"labels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"ready":{"type":"boolean"},"roles":{"type":"array","items":{"type":"string"}},"uid":{"type":["string","null"]},"usage":{"oneOf":[{"type":"null"},{"type":"object","properties":{"cpu":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]},"memory":{"oneOf":[{"type":"null"},{"type":"object","required":["value","unit"],"properties":{"unit":{"type":"string","enum":["count","percent","bytes","cores","milliseconds","requests-per-second"],"x-readme-ref-name":"MetricUnit"},"value":{"type":"number","format":"double"}},"x-readme-ref-name":"MetricSample"}]}},"x-readme-ref-name":"KubernetesNodeUsage"}]}},"x-readme-ref-name":"KubernetesClusterNodeStatus"}},"podCounts":{"type":"object","properties":{"current":{"type":["integer","null"],"format":"int32","minimum":0},"desired":{"type":["integer","null"],"format":"int32","minimum":0},"ready":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"ObservedCounts"},"region":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"WorkloadHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesClusterHeartbeatData"},"resourceType":{"type":"string","enum":["kubernetes-cluster"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","approximateCounts"],"properties":{"approximateCounts":{"type":"boolean"},"approximateDelayedMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateInFlightMessages":{"type":["integer","null"],"format":"int64","minimum":0},"approximateVisibleMessages":{"type":["integer","null"],"format":"int64","minimum":0},"contentBasedDeduplication":{"type":["boolean","null"]},"deduplicationScope":{"type":["string","null"]},"delaySeconds":{"type":["integer","null"],"format":"int32","minimum":0},"fifoQueue":{"type":["boolean","null"]},"fifoThroughputLimit":{"type":["string","null"]},"kmsDataKeyReusePeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"kmsMasterKeyId":{"type":["string","null"]},"maximumMessageSize":{"type":["integer","null"],"format":"int32","minimum":0},"messageRetentionPeriodSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"queueArn":{"type":["string","null"]},"queueUrl":{"type":["string","null"]},"receiveMessageWaitTimeSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"redriveAllowPolicy":{"type":["string","null"]},"redrivePolicy":{"type":["string","null"]},"region":{"type":["string","null"]},"sqsManagedSseEnabled":{"type":["boolean","null"]},"sseEnabled":{"type":["boolean","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"visibilityTimeoutSeconds":{"type":["integer","null"],"format":"int32","minimum":0}},"x-readme-ref-name":"AwsSqsQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsSqs"]}}}]},{"allOf":[{"type":"object","required":["status","topicName","topicLabels","subscriptionLabels","messageStorageAllowedPersistenceRegions","subscriptionPushAttributes"],"properties":{"endpoint":{"type":["string","null"]},"kmsKeyName":{"type":["string","null"]},"messageStorageAllowedPersistenceRegions":{"type":"array","items":{"type":"string"}},"messageStorageEnforceInTransit":{"type":["boolean","null"]},"projectId":{"type":["string","null"]},"schemaEncoding":{"type":["string","null"]},"schemaFirstRevisionId":{"type":["string","null"]},"schemaLastRevisionId":{"type":["string","null"]},"schemaName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"subscriptionAckDeadlineSeconds":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterMaxDeliveryAttempts":{"type":["integer","null"],"format":"int32","minimum":0},"subscriptionDeadLetterTopic":{"type":["string","null"]},"subscriptionDetached":{"type":["boolean","null"]},"subscriptionEnableMessageOrdering":{"type":["boolean","null"]},"subscriptionFilter":{"type":["string","null"]},"subscriptionFullName":{"type":["string","null"]},"subscriptionLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionMessageRetentionDuration":{"type":["string","null"]},"subscriptionName":{"type":["string","null"]},"subscriptionPushAttributes":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"subscriptionPushConfigPresent":{"type":["boolean","null"]},"subscriptionPushEndpoint":{"type":["string","null"]},"subscriptionPushNoWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionPushOidcAudience":{"type":["string","null"]},"subscriptionPushOidcServiceAccountEmail":{"type":["string","null"]},"subscriptionPushPubsubWrapperWriteMetadata":{"type":["boolean","null"]},"subscriptionRetainAckedMessages":{"type":["boolean","null"]},"subscriptionState":{"type":["string","null"]},"topicFullName":{"type":["string","null"]},"topicLabels":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"topicMessageRetentionDuration":{"type":["string","null"]},"topicName":{"type":"string"},"topicState":{"type":["string","null"]}},"x-readme-ref-name":"GcpPubSubQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpPubSub"]}}}]},{"allOf":[{"type":"object","required":["status","name","namespaceName"],"properties":{"accessedAt":{"type":["string","null"]},"activeMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"autoDeleteOnIdle":{"type":["string","null"]},"createdAt":{"type":["string","null"]},"deadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"deadLetteringOnMessageExpiration":{"type":["boolean","null"]},"defaultMessageTimeToLive":{"type":["string","null"]},"duplicateDetectionHistoryTimeWindow":{"type":["string","null"]},"enableBatchedOperations":{"type":["boolean","null"]},"enableExpress":{"type":["boolean","null"]},"enablePartitioning":{"type":["boolean","null"]},"endpoint":{"type":["string","null"]},"forwardDeadLetteredMessagesTo":{"type":["string","null"]},"forwardTo":{"type":["string","null"]},"lockDuration":{"type":["string","null"]},"maxDeliveryCount":{"type":["integer","null"],"format":"int32","minimum":0},"maxMessageSizeInKilobytes":{"type":["integer","null"],"format":"int64","minimum":0},"maxSizeInMegabytes":{"type":["integer","null"],"format":"int32","minimum":0},"messageCount":{"type":["integer","null"],"format":"int64","minimum":0},"name":{"type":"string"},"namespaceName":{"type":"string"},"queueStatus":{"type":["string","null"]},"requiresDuplicateDetection":{"type":["boolean","null"]},"requiresSession":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"scheduledMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"sizeInBytes":{"type":["integer","null"],"format":"int64","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"transferDeadLetterMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"transferMessageCount":{"type":["integer","null"],"format":"int64","minimum":0},"updatedAt":{"type":["string","null"]}},"x-readme-ref-name":"AzureServiceBusQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureServiceBus"]}}}]},{"allOf":[{"type":"object","required":["status","name"],"properties":{"name":{"type":"string"},"path":{"type":["string","null"]},"serviceStatus":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"}},"x-readme-ref-name":"LocalQueueHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"QueueHeartbeatData"},"resourceType":{"type":"string","enum":["queue"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","name","keySchema"],"properties":{"billingMode":{"type":["string","null"]},"deletionProtectionEnabled":{"type":["boolean","null"]},"globalSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"itemCount":{"type":["integer","null"],"format":"int64","minimum":0},"keySchema":{"type":"array","items":{"type":"object","required":["attributeName","keyType"],"properties":{"attributeName":{"type":"string"},"keyType":{"type":"string"}},"x-readme-ref-name":"AwsDynamoDbKeySchemaElement"}},"localSecondaryIndexCount":{"type":["integer","null"],"format":"int32","minimum":0},"name":{"type":"string"},"region":{"type":["string","null"]},"replicaCount":{"type":["integer","null"],"format":"int32","minimum":0},"restoreInProgress":{"type":["boolean","null"]},"sseStatus":{"type":["string","null"]},"sseType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"streamEnabled":{"type":["boolean","null"]},"streamViewType":{"type":["string","null"]},"tableArn":{"type":["string","null"]},"tableClass":{"type":["string","null"]},"tableSizeBytes":{"type":["integer","null"],"format":"int64","minimum":0},"tableStatus":{"type":["string","null"]},"ttlAttributeName":{"type":["string","null"]},"ttlStatus":{"type":["string","null"]}},"x-readme-ref-name":"AwsDynamoDbKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsDynamoDb"]}}}]},{"allOf":[{"type":"object","required":["status","databaseName","cmekEnabled","sourceInfoPresent"],"properties":{"appEngineIntegrationMode":{"type":["string","null"]},"cmekEnabled":{"type":"boolean"},"concurrencyMode":{"type":["string","null"]},"createTime":{"type":["string","null"]},"databaseEdition":{"type":["string","null"]},"databaseName":{"type":"string"},"databaseType":{"type":["string","null"]},"deleteProtectionState":{"type":["string","null"]},"deleteTime":{"type":["string","null"]},"earliestVersionTime":{"type":["string","null"]},"endpoint":{"type":["string","null"]},"locationId":{"type":["string","null"]},"pointInTimeRecoveryEnablement":{"type":["string","null"]},"projectId":{"type":["string","null"]},"sourceInfoPresent":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"updateTime":{"type":["string","null"]},"versionRetentionPeriod":{"type":["string","null"]}},"x-readme-ref-name":"GcpFirestoreKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpFirestore"]}}}]},{"allOf":[{"type":"object","required":["status","tableName","storageAccountName","tableExists"],"properties":{"endpoint":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"signedIdentifierCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"},"storageAccountKind":{"type":["string","null"]},"storageAccountLocation":{"type":["string","null"]},"storageAccountName":{"type":"string"},"storageAccountPrimaryStatus":{"type":["string","null"]},"storageAccountProvisioningState":{"type":["string","null"]},"storageAccountResourceId":{"type":["string","null"]},"tableExists":{"type":"boolean"},"tableName":{"type":"string"}},"x-readme-ref-name":"AzureTableKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureTable"]}}}]},{"allOf":[{"type":"object","required":["status","name","path","pathExists","cloudMetadataSupported"],"properties":{"cloudMetadataSupported":{"type":"boolean"},"isDirectory":{"type":["boolean","null"]},"name":{"type":"string"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"KvHeartbeatStatus"}},"x-readme-ref-name":"LocalKvHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"KvHeartbeatData"},"resourceType":{"type":"string","enum":["kv"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"description":"AWS Aurora Serverless v2 backend.","type":"object","required":["status","clusterIdentifier","neverPauses"],"properties":{"clusterIdentifier":{"type":"string"},"endpoint":{"type":["string","null"]},"engineVersion":{"type":["string","null"]},"neverPauses":{"type":"boolean","description":"True when a `minCapacity: 0` instance has not reached 0 ACU over the observation\nwindow — it is silently paying always-on prices (auto-pause verification)."},"serverlessCapacity":{"type":["number","null"],"format":"double","description":"Latest sampled `ServerlessDatabaseCapacity` (ACU)."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"AuroraPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["aurora"]}}}],"description":"AWS Aurora Serverless v2 backend."},{"allOf":[{"description":"GCP Cloud SQL backend.","type":"object","required":["status","instanceName"],"properties":{"databaseVersion":{"type":["string","null"]},"instanceName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudSqlPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["cloudSql"]}}}],"description":"GCP Cloud SQL backend."},{"allOf":[{"description":"Azure Flexible Server backend.","type":"object","required":["status","serverName"],"properties":{"serverName":{"type":"string"},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":["string","null"]}},"x-readme-ref-name":"AzureFlexibleServerPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["flexibleServer"]}}}],"description":"Azure Flexible Server backend."},{"allOf":[{"description":"Local embedded Postgres backend.","type":"object","required":["status","name","version","processRunning"],"properties":{"name":{"type":"string"},"port":{"type":["integer","null"],"format":"int32","minimum":0},"processRunning":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"PostgresHeartbeatStatus"},"version":{"type":"string"}},"x-readme-ref-name":"LocalPostgresHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}],"description":"Local embedded Postgres backend."}],"x-readme-ref-name":"PostgresHeartbeatData"},"resourceType":{"type":"string","enum":["postgres"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","accountId","region","prefix","parameterMetadataSampled"],"properties":{"accountId":{"type":"string"},"hasMoreParameters":{"type":["boolean","null"]},"latestModifiedAt":{"type":["string","null"],"format":"date-time"},"parameterMetadataSampled":{"type":"boolean"},"prefix":{"type":"string"},"region":{"type":"string"},"sampledAdvancedTierCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledKmsKeyMetadataPresentCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledParameterCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledSecureStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringCount":{"type":["integer","null"],"format":"int32","minimum":0},"sampledStringListCount":{"type":["integer","null"],"format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"AwsParameterStoreVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsParameterStore"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","prefix","secretMetadataListed"],"properties":{"location":{"type":"string"},"prefix":{"type":"string"},"projectId":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"GcpSecretManagerVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpSecretManager"]}}}]},{"allOf":[{"type":"object","required":["status","name","softDeleteEnabled","softDeleteRetentionDays","rbacAuthorizationEnabled","publicNetworkAccess","accessPolicyCount","privateEndpointConnectionCount","secretMetadataListed"],"properties":{"accessPolicyCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":["string","null"]},"name":{"type":"string"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"purgeProtectionEnabled":{"type":["boolean","null"]},"rbacAuthorizationEnabled":{"type":"boolean"},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secretMetadataListed":{"type":"boolean"},"skuFamily":{"type":["string","null"]},"skuName":{"type":["string","null"]},"softDeleteEnabled":{"type":"boolean"},"softDeleteRetentionDays":{"type":"integer","format":"int32"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"},"vaultUri":{"type":["string","null"]}},"x-readme-ref-name":"AzureKeyVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureKeyVault"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","prefix","secretMetadataListed"],"properties":{"namespace":{"type":"string"},"prefix":{"type":"string"},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"KubernetesSecretVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesSecret"]}}}]},{"allOf":[{"type":"object","required":["status","path","pathExists","secretMetadataListed"],"properties":{"isDirectory":{"type":["boolean","null"]},"modifiedAt":{"type":["string","null"],"format":"date-time"},"path":{"type":"string"},"pathExists":{"type":"boolean"},"readonly":{"type":["boolean","null"]},"secretMetadataListed":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"VaultHeartbeatStatus"}},"x-readme-ref-name":"LocalVaultHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"VaultHeartbeatData"},"resourceType":{"type":"string","enum":["vault"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","roleName","roleArn","roleId","path","createDate","assumeRolePolicyPresent","tagCount","managedTagCount","attachedPolicyCount","attachedPolicyNames","inlinePolicyCount","inlinePolicyNames","stackPermissionsApplied"],"properties":{"assumeRolePolicyPresent":{"type":"boolean"},"attachedPolicyCount":{"type":"integer","format":"int32","minimum":0},"attachedPolicyNames":{"type":"array","items":{"type":"string"}},"createDate":{"type":"string"},"description":{"type":["string","null"]},"inlinePolicyCount":{"type":"integer","format":"int32","minimum":0},"inlinePolicyNames":{"type":"array","items":{"type":"string"}},"lastUsedDate":{"type":["string","null"]},"lastUsedRegion":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"maxSessionDuration":{"type":["integer","null"],"format":"int32"},"path":{"type":"string"},"permissionsBoundaryArn":{"type":["string","null"]},"permissionsBoundaryType":{"type":["string","null"]},"roleArn":{"type":"string"},"roleId":{"type":"string"},"roleName":{"type":"string"},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tagCount":{"type":"integer","format":"int32","minimum":0}},"x-readme-ref-name":"AwsIamRoleServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","email","projectBindingCount","projectRoles","serviceAccountBindingCount","serviceAccountRoles"],"properties":{"description":{"type":["string","null"]},"disabled":{"type":["boolean","null"]},"displayName":{"type":["string","null"]},"email":{"type":"string"},"etag":{"type":["string","null"]},"name":{"type":["string","null"]},"oauth2ClientId":{"type":["string","null"]},"projectBindingCount":{"type":"integer","format":"int32","minimum":0},"projectId":{"type":["string","null"]},"projectRoles":{"type":"array","items":{"type":"string"}},"serviceAccountBindingCount":{"type":"integer","format":"int32","minimum":0},"serviceAccountRoles":{"type":"array","items":{"type":"string"}},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"uniqueId":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceId","resourceGroup","location","managedTagCount","roleAssignmentCount","roleAssignmentIds","customRoleDefinitionCount","customRoleDefinitionIds","stackPermissionsApplied"],"properties":{"clientId":{"type":["string","null"]},"customRoleDefinitionCount":{"type":"integer","format":"int32","minimum":0},"customRoleDefinitionIds":{"type":"array","items":{"type":"string"}},"isolationScope":{"type":["string","null"]},"location":{"type":"string"},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"principalId":{"type":["string","null"]},"resourceGroup":{"type":"string"},"resourceId":{"type":"string"},"roleAssignmentCount":{"type":"integer","format":"int32","minimum":0},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"stackPermissionsApplied":{"type":"boolean"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"},"tenantId":{"type":["string","null"]},"type":{"type":["string","null"]}},"x-readme-ref-name":"AzureManagedIdentityServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]},{"allOf":[{"type":"object","required":["status","identity","configured"],"properties":{"configured":{"type":"boolean"},"identity":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceAccountHeartbeatStatus"}},"x-readme-ref-name":"LocalServiceAccountHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ServiceAccountHeartbeatData"},"resourceType":{"type":"string","enum":["service-account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","publicSubnetIds","privateSubnetIds","availabilityZones","routeTableCount","isByoVpc"],"properties":{"availabilityZones":{"type":"array","items":{"type":"string"}},"cidrBlock":{"type":["string","null"]},"internetGatewayId":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"natGatewayId":{"type":["string","null"]},"privateSubnetIds":{"type":"array","items":{"type":"string"}},"publicSubnetIds":{"type":"array","items":{"type":"string"}},"routeTableCount":{"type":"integer","format":"int32","minimum":0},"securityGroupId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vpcId":{"type":["string","null"]},"vpcState":{"type":["string","null"]}},"x-readme-ref-name":"AwsVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVpc"],"properties":{"cidrBlock":{"type":["string","null"]},"cloudNatName":{"type":["string","null"]},"firewallName":{"type":["string","null"]},"isByoVpc":{"type":"boolean"},"networkName":{"type":["string","null"]},"networkSelfLink":{"type":["string","null"]},"region":{"type":["string","null"]},"routerName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"subnetworkName":{"type":["string","null"]},"subnetworkSelfLink":{"type":["string","null"]}},"x-readme-ref-name":"GcpVpcNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpVpc"]}}}]},{"allOf":[{"type":"object","required":["status","isByoVnet"],"properties":{"applicationGatewaySubnetName":{"type":["string","null"]},"cidrBlock":{"type":["string","null"]},"isByoVnet":{"type":"boolean"},"lastByoVnetVerificationErrorCode":{"type":["string","null"]},"location":{"type":["string","null"]},"natGatewayId":{"type":["string","null"]},"nsgId":{"type":["string","null"]},"privateEndpointSubnetName":{"type":["string","null"]},"privateSubnetName":{"type":["string","null"]},"publicIpId":{"type":["string","null"]},"publicSubnetName":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"NetworkHeartbeatStatus"},"vnetName":{"type":["string","null"]},"vnetResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureVnetNetworkHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureVnet"]}}}]}],"x-readme-ref-name":"NetworkHeartbeatData"},"resourceType":{"type":"string","enum":["network"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","managementPermissionsApplied"],"properties":{"managementPermissionsApplied":{"type":"boolean"},"roleArn":{"type":["string","null"]},"roleName":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"AwsRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsIamRole"]}}}]},{"allOf":[{"type":"object","required":["status","roleBound","impersonationGranted"],"properties":{"impersonationGranted":{"type":"boolean"},"roleBound":{"type":"boolean"},"serviceAccountEmail":{"type":["string","null"]},"serviceAccountUniqueId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"}},"x-readme-ref-name":"GcpRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceAccount"]}}}]},{"allOf":[{"type":"object","required":["status","roleAssignmentIds"],"properties":{"ficName":{"type":["string","null"]},"roleAssignmentIds":{"type":"array","items":{"type":"string"}},"roleDefinitionId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"RemoteStackManagementHeartbeatStatus"},"tenantId":{"type":["string","null"]},"uamiClientId":{"type":["string","null"]},"uamiPrincipalId":{"type":["string","null"]},"uamiResourceId":{"type":["string","null"]}},"x-readme-ref-name":"AzureRemoteStackManagementHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureManagedIdentity"]}}}]}],"x-readme-ref-name":"RemoteStackManagementHeartbeatData"},"resourceType":{"type":"string","enum":["remote-stack-management"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","registryId","region","registryUri","repositoryPrefix","repositoryCount","repositoriesTruncated","repositories"],"properties":{"pullRoleArn":{"type":["string","null"]},"pushRoleArn":{"type":["string","null"]},"region":{"type":"string"},"registryId":{"type":"string"},"registryUri":{"type":"string"},"repositories":{"type":"array","items":{"type":"object","required":["repositoryArn","registryId","repositoryName","repositoryUri","createdAt","kmsKeyPresent"],"properties":{"createdAt":{"type":"number","format":"double"},"encryptionType":{"type":["string","null"]},"imageTagMutability":{"type":["string","null"]},"kmsKeyPresent":{"type":"boolean"},"registryId":{"type":"string"},"repositoryArn":{"type":"string"},"repositoryName":{"type":"string"},"repositoryUri":{"type":"string"},"scanOnPush":{"type":["boolean","null"]}},"x-readme-ref-name":"AwsEcrRepositoryHeartbeatData"}},"repositoriesTruncated":{"type":"boolean"},"repositoryCount":{"type":"integer","format":"int32","minimum":0},"repositoryPrefix":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"AwsEcrArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsEcr"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","repositoryId","labelCount","cleanupPolicyCount","kmsKeyNamePresent","iamPolicyEtagPresent","iamBindingCount","iamRoles"],"properties":{"cleanupPolicyCount":{"type":"integer","format":"int32","minimum":0},"cleanupPolicyDryRun":{"type":["boolean","null"]},"createTime":{"type":["string","null"]},"description":{"type":["string","null"]},"format":{"type":["string","null"]},"iamBindingCount":{"type":"integer","format":"int32","minimum":0},"iamPolicyEtagPresent":{"type":"boolean"},"iamRoles":{"type":"array","items":{"type":"string"}},"kmsKeyNamePresent":{"type":"boolean"},"labelCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"mode":{"type":["string","null"]},"name":{"type":["string","null"]},"projectId":{"type":"string"},"pullServiceAccountEmail":{"type":["string","null"]},"pushServiceAccountEmail":{"type":["string","null"]},"repositoryId":{"type":"string"},"satisfiesPzs":{"type":["boolean","null"]},"sizeBytes":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"updateTime":{"type":["string","null"]}},"x-readme-ref-name":"GcpArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpArtifactRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","name","resourceGroup","location","skuName","adminUserEnabled","anonymousPullEnabled","publicNetworkAccess","networkRuleBypassOptions","ipRuleCount","encryptionKeyVaultUriPresent","encryptionKeyIdentifierPresent","policiesPresent","policyCount","privateEndpointConnectionCount","dataEndpointHostNames","zoneRedundancy","managedTagCount"],"properties":{"adminUserEnabled":{"type":"boolean"},"anonymousPullEnabled":{"type":"boolean"},"creationDate":{"type":["string","null"]},"dataEndpointEnabled":{"type":["boolean","null"]},"dataEndpointHostNames":{"type":"array","items":{"type":"string"}},"encryptionKeyIdentifierPresent":{"type":"boolean"},"encryptionKeyVaultUriPresent":{"type":"boolean"},"encryptionStatus":{"type":["string","null"]},"ipRuleCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"loginServer":{"type":["string","null"]},"managedTagCount":{"type":"integer","format":"int32","minimum":0},"name":{"type":"string"},"networkRuleBypassOptions":{"type":"string"},"networkRuleDefaultAction":{"type":["string","null"]},"policiesPresent":{"type":"boolean"},"policyCount":{"type":"integer","format":"int32","minimum":0},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":"string"},"resourceGroup":{"type":"string"},"resourceId":{"type":["string","null"]},"skuName":{"type":"string"},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"},"type":{"type":["string","null"]},"zoneRedundancy":{"type":"string"}},"x-readme-ref-name":"AzureContainerRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerRegistry"]}}}]},{"allOf":[{"type":"object","required":["status","registryUrl","reachable"],"properties":{"reachable":{"type":"boolean"},"registryUrl":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ArtifactRegistryHeartbeatStatus"}},"x-readme-ref-name":"LocalArtifactRegistryHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["local"]}}}]}],"x-readme-ref-name":"ArtifactRegistryHeartbeatData"},"resourceType":{"type":"string","enum":["artifact-registry"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectName","environmentVariableCount","serviceRolePresent","encryptionKeyPresent"],"properties":{"artifactsEncryptionDisabled":{"type":["boolean","null"]},"artifactsType":{"type":["string","null"]},"cloudWatchLogsStatus":{"type":["string","null"]},"computeType":{"type":["string","null"]},"created":{"type":["number","null"],"format":"double"},"description":{"type":["string","null"]},"encryptionKeyPresent":{"type":"boolean"},"environmentImage":{"type":["string","null"]},"environmentType":{"type":["string","null"]},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"imagePullCredentialsType":{"type":["string","null"]},"lastModified":{"type":["number","null"],"format":"double"},"privilegedMode":{"type":["boolean","null"]},"projectArn":{"type":["string","null"]},"projectName":{"type":"string"},"queuedTimeoutInMinutes":{"type":["integer","null"],"format":"int32"},"s3LogsStatus":{"type":["string","null"]},"serviceRolePresent":{"type":"boolean"},"sourceType":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"timeoutInMinutes":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"AwsCodeBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsCodeBuild"]}}}]},{"allOf":[{"type":"object","required":["status","projectId","location","buildConfigId","environmentVariableCount"],"properties":{"buildConfigId":{"type":"string"},"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"location":{"type":"string"},"projectId":{"type":"string"},"serviceAccount":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"GcpCloudBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpCloudBuild"]}}}]},{"allOf":[{"type":"object","required":["status","managedEnvironmentId","resourceGroupName","environmentVariableCount"],"properties":{"environmentVariableCount":{"type":"integer","format":"int32","minimum":0},"managedEnvironmentId":{"type":"string"},"managedIdentityId":{"type":["string","null"]},"resourceGroupName":{"type":"string"},"resourcePrefix":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"}},"x-readme-ref-name":"AzureContainerAppsBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureContainerApps"]}}}]},{"allOf":[{"type":"object","required":["status","jobName","namespace","conditionCount","events"],"properties":{"active":{"type":["integer","null"],"format":"int32"},"completionTime":{"type":["string","null"],"format":"date-time"},"conditionCount":{"type":"integer","format":"int32","minimum":0},"events":{"type":"array","items":{"type":"object","required":["reason","message"],"properties":{"count":{"type":["integer","null"],"format":"int32"},"eventTime":{"type":["string","null"],"format":"date-time"},"firstTimestamp":{"type":["string","null"],"format":"date-time"},"involvedObject":{"oneOf":[{"type":"null"},{"type":"object","properties":{"apiVersion":{"type":["string","null"]},"fieldPath":{"type":["string","null"]},"kind":{"type":["string","null"]},"name":{"type":["string","null"]},"namespace":{"type":["string","null"]},"resourceVersion":{"type":["string","null"]},"uid":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventInvolvedObject"}]},"lastTimestamp":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"},"raw":{"oneOf":[{"type":"null"},{"x-readme-ref-name":"Value"}]},"reason":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"type":"object","properties":{"component":{"type":["string","null"]},"host":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSource"}]},"type":{"type":["string","null"]}},"x-readme-ref-name":"KubernetesEventSnapshot"}},"failed":{"type":["integer","null"],"format":"int32"},"imageDigest":{"type":["string","null"]},"jobName":{"type":"string"},"namespace":{"type":"string"},"startTime":{"type":["string","null"],"format":"date-time"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"BuildHeartbeatStatus"},"succeeded":{"type":["integer","null"],"format":"int32"}},"x-readme-ref-name":"KubernetesBuildHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["kubernetesJob"]}}}]}],"x-readme-ref-name":"BuildHeartbeatData"},"resourceType":{"type":"string","enum":["build"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","projectId","serviceName","enabled"],"properties":{"enabled":{"type":"boolean"},"lastOperationName":{"type":["string","null"]},"projectId":{"type":"string"},"serviceName":{"type":"string"},"serviceResourceName":{"type":["string","null"]},"state":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"},"title":{"type":["string","null"]}},"x-readme-ref-name":"GcpServiceUsageActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpServiceUsage"]}}}]},{"allOf":[{"type":"object","required":["status","namespace","resourceTypeCount","registered"],"properties":{"namespace":{"type":"string"},"providerId":{"type":["string","null"]},"registered":{"type":"boolean"},"registrationPolicy":{"type":["string","null"]},"registrationState":{"type":["string","null"]},"resourceTypeCount":{"type":"integer","format":"int32","minimum":0},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"ServiceActivationHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceProviderActivationHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureResourceProvider"]}}}]}],"x-readme-ref-name":"ServiceActivationHeartbeatData"},"resourceType":{"type":"string","enum":["service_activation"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","managedTags"],"properties":{"location":{"type":["string","null"]},"managedTags":{"type":"object","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatStatus"}},"x-readme-ref-name":"AzureResourceGroupHeartbeatData"},"resourceType":{"type":"string","enum":["azure_resource_group"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","primaryEndpoints","secondaryEndpoints"],"properties":{"allowBlobPublicAccess":{"type":["boolean","null"]},"allowSharedKeyAccess":{"type":["boolean","null"]},"encryptionKeySource":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"networkBypass":{"type":["string","null"]},"networkDefaultAction":{"type":["string","null"]},"networkIpRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkResourceAccessRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"networkVirtualNetworkRuleCount":{"type":["integer","null"],"format":"int32","minimum":0},"primaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"requireInfrastructureEncryption":{"type":["boolean","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"secondaryEndpoints":{"type":"object","properties":{"blob":{"type":["string","null"]},"dfs":{"type":["string","null"]},"file":{"type":["string","null"]},"queue":{"type":["string","null"]},"table":{"type":["string","null"]},"web":{"type":["string","null"]}},"x-readme-ref-name":"AzureStorageAccountEndpoints"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"StorageHeartbeatStatus"},"supportsHttpsTrafficOnly":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureStorageAccountHeartbeatData"},"resourceType":{"type":"string","enum":["azure_storage_account"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","workloadProfileCount","workloadProfiles"],"properties":{"customDomainVerificationId":{"type":["string","null"]},"defaultDomain":{"type":["string","null"]},"eventStreamEndpoint":{"type":["string","null"]},"infrastructureResourceGroup":{"type":["string","null"]},"kind":{"type":["string","null"]},"location":{"type":["string","null"]},"name":{"type":"string"},"provisioningState":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"staticIp":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatStatus"},"workloadProfileCount":{"type":"integer","format":"int32","minimum":0},"workloadProfiles":{"type":"array","items":{"type":"object","required":["name","workloadProfileType"],"properties":{"maximumCount":{"type":["integer","null"],"format":"int32"},"minimumCount":{"type":["integer","null"],"format":"int32"},"name":{"type":"string"},"workloadProfileType":{"type":"string"}},"x-readme-ref-name":"AzureContainerAppsEnvironmentWorkloadProfile"}},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureContainerAppsEnvironmentHeartbeatData"},"resourceType":{"type":"string","enum":["azure_container_apps_environment"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"type":"object","required":["status","name","privateEndpointConnectionCount"],"properties":{"createdAt":{"type":["string","null"]},"disableLocalAuth":{"type":["boolean","null"]},"location":{"type":["string","null"]},"metricId":{"type":["string","null"]},"minimumTlsVersion":{"type":["string","null"]},"name":{"type":"string"},"namespaceStatus":{"type":["string","null"]},"premiumMessagingPartitions":{"type":["integer","null"],"format":"int32"},"privateEndpointConnectionCount":{"type":"integer","format":"int32","minimum":0},"provisioningState":{"type":["string","null"]},"publicNetworkAccess":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"resourceId":{"type":["string","null"]},"serviceBusEndpoint":{"type":["string","null"]},"skuCapacity":{"type":["integer","null"],"format":"int32"},"skuName":{"type":["string","null"]},"skuTier":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"QueueHeartbeatStatus"},"updatedAt":{"type":["string","null"]},"zoneRedundant":{"type":["boolean","null"]}},"x-readme-ref-name":"AzureServiceBusNamespaceHeartbeatData"},"resourceType":{"type":"string","enum":["azure_service_bus_namespace"]}}},{"type":"object","required":["data","resourceType"],"properties":{"data":{"oneOf":[{"allOf":[{"type":"object","required":["status","region"],"properties":{"region":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AwsBedrockAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["awsBedrock"]}}}]},{"allOf":[{"type":"object","required":["status","project","location"],"properties":{"location":{"type":"string"},"project":{"type":"string"},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"GcpVertexAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["gcpVertex"]}}}]},{"allOf":[{"type":"object","required":["status","accountName"],"properties":{"accountName":{"type":"string"},"endpoint":{"type":["string","null"]},"location":{"type":["string","null"]},"resourceGroup":{"type":["string","null"]},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"AzureFoundryAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["azureFoundry"]}}}]},{"allOf":[{"type":"object","required":["status","provider"],"properties":{"provider":{"type":"string","description":"The BYO-key provider serving this binding (e.g. \"openai\"). Used on the Local\nplatform, where the app brings its own provider key instead of an ambient cloud."},"status":{"type":"object","required":["health","lifecycle","stale","partial","collectionIssues"],"properties":{"collectionIssues":{"type":"array","items":{"type":"object","required":["source","reason","severity","message"],"properties":{"message":{"type":"string"},"reason":{"type":"string","enum":["forbidden","not-installed","api-unavailable","collection-failed","timed-out"],"x-readme-ref-name":"HeartbeatCollectionIssueReason"},"severity":{"type":"string","enum":["info","warning","error"],"x-readme-ref-name":"HeartbeatIssueSeverity"},"source":{"type":"string"}},"x-readme-ref-name":"HeartbeatCollectionIssue"}},"health":{"type":"string","enum":["unknown","healthy","degraded","unhealthy"],"x-readme-ref-name":"ObservedHealth"},"lifecycle":{"type":"string","enum":["unknown","creating","updating","running","scaling","stopping","stopped","deleting","deleted","failed"],"x-readme-ref-name":"ProviderLifecycleState"},"message":{"type":["string","null"]},"partial":{"type":"boolean"},"stale":{"type":"boolean"}},"x-readme-ref-name":"AiHeartbeatStatus"}},"x-readme-ref-name":"ExternalAiHeartbeatData"},{"type":"object","required":["backend"],"properties":{"backend":{"type":"string","enum":["external"]}}}]}],"x-readme-ref-name":"AiHeartbeatData"},"resourceType":{"type":"string","enum":["ai"]}}}],"x-readme-ref-name":"ResourceHeartbeatData"} \ No newline at end of file diff --git a/packages/core/src/generated/zod/ai-heartbeat-data-schema.ts b/packages/core/src/generated/zod/ai-heartbeat-data-schema.ts new file mode 100644 index 000000000..eaf6c0c3b --- /dev/null +++ b/packages/core/src/generated/zod/ai-heartbeat-data-schema.ts @@ -0,0 +1,22 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { AwsBedrockAiHeartbeatDataSchema } from "./aws-bedrock-ai-heartbeat-data-schema.js"; +import { AzureFoundryAiHeartbeatDataSchema } from "./azure-foundry-ai-heartbeat-data-schema.js"; +import { ExternalAiHeartbeatDataSchema } from "./external-ai-heartbeat-data-schema.js"; +import { GcpVertexAiHeartbeatDataSchema } from "./gcp-vertex-ai-heartbeat-data-schema.js"; + +export const AiHeartbeatDataSchema = z.union([z.lazy(() => AwsBedrockAiHeartbeatDataSchema).and(z.object({ + "backend": z.enum(["awsBedrock"]) + })), z.lazy(() => GcpVertexAiHeartbeatDataSchema).and(z.object({ + "backend": z.enum(["gcpVertex"]) + })), z.lazy(() => AzureFoundryAiHeartbeatDataSchema).and(z.object({ + "backend": z.enum(["azureFoundry"]) + })), z.lazy(() => ExternalAiHeartbeatDataSchema).and(z.object({ + "backend": z.enum(["external"]) + }))]) + +export type AiHeartbeatData = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/ai-heartbeat-status-schema.ts b/packages/core/src/generated/zod/ai-heartbeat-status-schema.ts new file mode 100644 index 000000000..699273750 --- /dev/null +++ b/packages/core/src/generated/zod/ai-heartbeat-status-schema.ts @@ -0,0 +1,26 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { HeartbeatCollectionIssueSchema } from "./heartbeat-collection-issue-schema.js"; +import { ObservedHealthSchema } from "./observed-health-schema.js"; +import { ProviderLifecycleStateSchema } from "./provider-lifecycle-state-schema.js"; + +export const AiHeartbeatStatusSchema = z.object({ + get "collectionIssues"(){ + return z.array(HeartbeatCollectionIssueSchema) + }, +get "health"(){ + return ObservedHealthSchema + }, +get "lifecycle"(){ + return ProviderLifecycleStateSchema + }, +"message": z.string().nullish(), +"partial": z.boolean(), +"stale": z.boolean() + }) + +export type AiHeartbeatStatus = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/ai-outputs-schema.ts b/packages/core/src/generated/zod/ai-outputs-schema.ts new file mode 100644 index 000000000..51615a4be --- /dev/null +++ b/packages/core/src/generated/zod/ai-outputs-schema.ts @@ -0,0 +1,17 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; + +/** + * @description Outputs generated by a successfully provisioned AI Gateway resource. + */ +export const AiOutputsSchema = z.object({ + "account": z.string().describe("The provider account or project identifier, if applicable.").nullish(), +"endpoint": z.string().describe("The provider endpoint URL, if applicable.").nullish(), +"provider": z.string().describe("The AI provider name (e.g., \"bedrock\", \"vertex\", \"foundry\", \"external\").") + }).describe("Outputs generated by a successfully provisioned AI Gateway resource.") + +export type AiOutputs = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/ai-schema.ts b/packages/core/src/generated/zod/ai-schema.ts new file mode 100644 index 000000000..8ea89a00b --- /dev/null +++ b/packages/core/src/generated/zod/ai-schema.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { FinetuneSpecSchema } from "./finetune-spec-schema.js"; + +/** + * @description Represents an AI Gateway resource that provides a unified interface to\nmanaged AI inference services across cloud providers.\n\nBYO-key external providers (OpenAI/Anthropic) are NOT declared here. Like any\nother BYO infrastructure (e.g. external Redis for `kv`), an external AI\nprovider is supplied at deploy time as an `ExternalBinding::Ai` in the\nstack\'s external-bindings map; the executor then skips the cloud controller.\n\nWhen `finetune` is set, the resource also tunes a base model in the\ncustomer\'s cloud and serves the result through the same gateway (see\n[`FinetuneSpec`]). + */ +export const AiSchema = z.object({ + get "finetune"(){ + return z.union([FinetuneSpecSchema, z.null()]).optional() + }, +"id": z.string().describe("Identifier for the AI resource. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]).\nMaximum 64 characters.") + }).describe("Represents an AI Gateway resource that provides a unified interface to\nmanaged AI inference services across cloud providers.\n\nBYO-key external providers (OpenAI/Anthropic) are NOT declared here. Like any\nother BYO infrastructure (e.g. external Redis for `kv`), an external AI\nprovider is supplied at deploy time as an `ExternalBinding::Ai` in the\nstack's external-bindings map; the executor then skips the cloud controller.\n\nWhen `finetune` is set, the resource also tunes a base model in the\ncustomer's cloud and serves the result through the same gateway (see\n[`FinetuneSpec`]).") + +export type Ai = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/aws-bedrock-ai-heartbeat-data-schema.ts b/packages/core/src/generated/zod/aws-bedrock-ai-heartbeat-data-schema.ts new file mode 100644 index 000000000..65a09264b --- /dev/null +++ b/packages/core/src/generated/zod/aws-bedrock-ai-heartbeat-data-schema.ts @@ -0,0 +1,16 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { AiHeartbeatStatusSchema } from "./ai-heartbeat-status-schema.js"; + +export const AwsBedrockAiHeartbeatDataSchema = z.object({ + "region": z.string(), +get "status"(){ + return AiHeartbeatStatusSchema + } + }) + +export type AwsBedrockAiHeartbeatData = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/aws-network-import-data-schema.ts b/packages/core/src/generated/zod/aws-network-import-data-schema.ts index 053de084c..c3916d739 100644 --- a/packages/core/src/generated/zod/aws-network-import-data-schema.ts +++ b/packages/core/src/generated/zod/aws-network-import-data-schema.ts @@ -22,9 +22,9 @@ export const AwsNetworkImportDataSchema = z.object({ "publicSubnetIds": z.array(z.string()).describe("Public subnet IDs."), "securityGroupId": z.string().describe("Security group ID for private workloads.").nullish(), "subnetsByFailureDomain": z.optional(z.object({ - + }).catchall(z.lazy(() => AwsFailureDomainSubnetsSchema).describe("AWS subnets grouped by their real availability zone.")).describe("Exact subnet membership keyed by real availability zone.")), "vpcId": z.string().describe("VPC ID. Absent when default-VPC mode defers lookup to AWS at runtime.").nullish() }).describe("AWS Network ImportData.") -export type AwsNetworkImportData = z.infer +export type AwsNetworkImportData = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/azure-foundry-ai-heartbeat-data-schema.ts b/packages/core/src/generated/zod/azure-foundry-ai-heartbeat-data-schema.ts new file mode 100644 index 000000000..ebba00fb9 --- /dev/null +++ b/packages/core/src/generated/zod/azure-foundry-ai-heartbeat-data-schema.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { AiHeartbeatStatusSchema } from "./ai-heartbeat-status-schema.js"; + +export const AzureFoundryAiHeartbeatDataSchema = z.object({ + "accountName": z.string(), +"endpoint": z.string().nullish(), +"location": z.string().nullish(), +"resourceGroup": z.string().nullish(), +get "status"(){ + return AiHeartbeatStatusSchema + } + }) + +export type AzureFoundryAiHeartbeatData = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/compute-cluster-schema.ts b/packages/core/src/generated/zod/compute-cluster-schema.ts index fc1ea46f5..1dbfb5592 100644 --- a/packages/core/src/generated/zod/compute-cluster-schema.ts +++ b/packages/core/src/generated/zod/compute-cluster-schema.ts @@ -15,12 +15,12 @@ export const ComputeClusterSchema = z.object({ }, "containerCidr": z.string().describe("Container CIDR block for internal container networking.\nAuto-generated as \"10.244.0.0/16\" if not specified.\nEach machine gets a /24 subnet from this range.").nullish(), "failureDomainSpread": z.optional(z.object({ - + }).catchall(z.int().min(0)).describe("Requested failure-domain spread keyed by capacity group.\nEmpty preserves the existing aggregate layout.")), "id": z.string().describe("Unique identifier for the container cluster.\nMust contain only alphanumeric characters, hyphens, and underscores."), "selectedFailureDomains": z.optional(z.object({ - + }).catchall(z.array(z.string())).describe("Concrete provider failure domains selected during setup, keyed by capacity group.\nEmpty preserves the existing aggregate layout when no spread policy is configured.")) }).describe("ComputeCluster resource for running long-running container workloads.\n\nA ComputeCluster provides the setup-owned machine boundary for containers.\nAlien may manage the worker fleet inside that boundary when setup grants\n`compute-cluster/management`.\n\n## Architecture\n\n- **Setup** creates cloud resources: ASGs/MIGs/VMSSs, IAM roles, security groups\n- **Alien** manages allowed fleet operations: machine count and runtime\n machine image rollout\n- A node agent runs on each machine from the selected runtime image channel\n\n## Example\n\n```rust\nuse alien_core::{CapacityGroup, ComputeCluster, MachineProfile};\n\nlet cluster = ComputeCluster::new(\"compute\".to_string())\n .capacity_group(CapacityGroup {\n group_id: \"general\".to_string(),\n instance_type: None,\n profile: Some(MachineProfile {\n cpu: \"4.0\".to_string(),\n memory_bytes: 16 * 1024 * 1024 * 1024,\n ephemeral_storage_bytes: 20 * 1024 * 1024 * 1024,\n architecture: None,\n gpu: None,\n }),\n min_size: 1,\n max_size: 5,\n scale_policy: None,\n nested_virtualization: None,\n })\n .build();\n```") -export type ComputeCluster = z.infer +export type ComputeCluster = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/external-ai-heartbeat-data-schema.ts b/packages/core/src/generated/zod/external-ai-heartbeat-data-schema.ts new file mode 100644 index 000000000..858efe78f --- /dev/null +++ b/packages/core/src/generated/zod/external-ai-heartbeat-data-schema.ts @@ -0,0 +1,16 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { AiHeartbeatStatusSchema } from "./ai-heartbeat-status-schema.js"; + +export const ExternalAiHeartbeatDataSchema = z.object({ + "provider": z.string().describe("The BYO-key provider serving this binding (e.g. \"openai\"). Used on the Local\nplatform, where the app brings its own provider key instead of an ambient cloud."), +get "status"(){ + return AiHeartbeatStatusSchema + } + }) + +export type ExternalAiHeartbeatData = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/finetune-method-schema.ts b/packages/core/src/generated/zod/finetune-method-schema.ts new file mode 100644 index 000000000..152c63309 --- /dev/null +++ b/packages/core/src/generated/zod/finetune-method-schema.ts @@ -0,0 +1,13 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; + +/** + * @description The fine-tuning method applied to the base model.\n\nThe gateway-side controllers map each variant onto the provider\'s native\ntechnique: `Sft` is supervised fine-tuning (all three clouds), `Dpo` is\ndirect preference optimization (Bedrock/Foundry), and `Lora` requests a\nparameter-efficient adapter where the provider exposes it. Providers that\nonly implement a subset reject unsupported methods at job-submit time. + */ +export const FinetuneMethodSchema = z.enum(["sft", "dpo", "lora"]).describe("The fine-tuning method applied to the base model.\n\nThe gateway-side controllers map each variant onto the provider's native\ntechnique: `Sft` is supervised fine-tuning (all three clouds), `Dpo` is\ndirect preference optimization (Bedrock/Foundry), and `Lora` requests a\nparameter-efficient adapter where the provider exposes it. Providers that\nonly implement a subset reject unsupported methods at job-submit time.") + +export type FinetuneMethod = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/finetune-spec-schema.ts b/packages/core/src/generated/zod/finetune-spec-schema.ts new file mode 100644 index 000000000..8767ce2df --- /dev/null +++ b/packages/core/src/generated/zod/finetune-spec-schema.ts @@ -0,0 +1,22 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { FinetuneMethodSchema } from "./finetune-method-schema.js"; + +/** + * @description Declares that an [`Ai`] resource should fine-tune a base model in the\ncustomer\'s cloud before serving it.\n\nThis is a *capability declaration*, not a deploy-time trigger: a resource\nwith a `finetune` spec provisions and is Ready immediately (no job runs at\ndeploy). The declaration flows to the gateway as a fine-tuning capability;\nthe app then starts a job at runtime by calling `ai(\"\").finetune(...)`,\nwhich the gateway submits to the provider (Bedrock\n`CreateModelCustomizationJob`, Vertex tuning job, or Foundry\n`fine_tuning.jobs`) under the workload\'s ambient identity, reading the\ntraining data from the customer-owned [`Storage`](crate::Storage) bucket\n(S3 / GCS / Blob) referenced by `training_data`. Once a job completes, the\ngateway serves the tuned model under `served_model_id`, rediscovering it by\nconvention. Base-model inference is unaffected — an `Ai` without a\n`finetune` spec behaves exactly as before. + */ +export const FinetuneSpecSchema = z.object({ + "baseModel": z.string().describe("Provider-native base-model identifier to tune (e.g. an Amazon Nova model\nid on Bedrock, a Gemini model on Vertex, or a gpt-4o family model on\nFoundry). Validated against the target cloud when a runtime job is submitted."), +get "method"(){ + return FinetuneMethodSchema.describe("The fine-tuning method applied to the base model.\n\nThe gateway-side controllers map each variant onto the provider's native\ntechnique: `Sft` is supervised fine-tuning (all three clouds), `Dpo` is\ndirect preference optimization (Bedrock/Foundry), and `Lora` requests a\nparameter-efficient adapter where the provider exposes it. Providers that\nonly implement a subset reject unsupported methods at job-submit time.").optional() + }, +"servedModelId": z.string().describe("The public model id apps use to invoke the tuned model through the\ngateway (the `model` field in an OpenAI-compatible request). Defaults to\n`-tuned`.").nullish(), +"trainingData": z.string().describe("The storage resource holding the JSONL training dataset. The gateway\nreads it from the customer bucket the storage resolves to; the data\nnever leaves the customer's cloud."), +"trainingKey": z.optional(z.string().describe("Object key of the training file within `training_data`.\nDefaults to `training.jsonl`.")) + }).describe("Declares that an [`Ai`] resource should fine-tune a base model in the\ncustomer's cloud before serving it.\n\nThis is a *capability declaration*, not a deploy-time trigger: a resource\nwith a `finetune` spec provisions and is Ready immediately (no job runs at\ndeploy). The declaration flows to the gateway as a fine-tuning capability;\nthe app then starts a job at runtime by calling `ai(\"\").finetune(...)`,\nwhich the gateway submits to the provider (Bedrock\n`CreateModelCustomizationJob`, Vertex tuning job, or Foundry\n`fine_tuning.jobs`) under the workload's ambient identity, reading the\ntraining data from the customer-owned [`Storage`](crate::Storage) bucket\n(S3 / GCS / Blob) referenced by `training_data`. Once a job completes, the\ngateway serves the tuned model under `served_model_id`, rediscovering it by\nconvention. Base-model inference is unaffected — an `Ai` without a\n`finetune` spec behaves exactly as before.") + +export type FinetuneSpec = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/gcp-vertex-ai-heartbeat-data-schema.ts b/packages/core/src/generated/zod/gcp-vertex-ai-heartbeat-data-schema.ts new file mode 100644 index 000000000..fbea5e604 --- /dev/null +++ b/packages/core/src/generated/zod/gcp-vertex-ai-heartbeat-data-schema.ts @@ -0,0 +1,17 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; +import { AiHeartbeatStatusSchema } from "./ai-heartbeat-status-schema.js"; + +export const GcpVertexAiHeartbeatDataSchema = z.object({ + "location": z.string(), +"project": z.string(), +get "status"(){ + return AiHeartbeatStatusSchema + } + }) + +export type GcpVertexAiHeartbeatData = z.infer \ No newline at end of file diff --git a/packages/core/src/generated/zod/index.ts b/packages/core/src/generated/zod/index.ts index 23ccccc56..b8369ca6a 100644 --- a/packages/core/src/generated/zod/index.ts +++ b/packages/core/src/generated/zod/index.ts @@ -1,4 +1,8 @@ export type { AgentStatus } from "./agent-status-schema.js"; +export type { AiHeartbeatData } from "./ai-heartbeat-data-schema.js"; +export type { AiHeartbeatStatus } from "./ai-heartbeat-status-schema.js"; +export type { AiOutputs } from "./ai-outputs-schema.js"; +export type { Ai } from "./ai-schema.js"; export type { AlienError } from "./alien-error-schema.js"; export type { AlienEvent } from "./alien-event-schema.js"; export type { Architecture } from "./architecture-schema.js"; @@ -8,6 +12,7 @@ export type { ArtifactRegistryOutputs } from "./artifact-registry-outputs-schema export type { ArtifactRegistry } from "./artifact-registry-schema.js"; export type { AuroraPostgresHeartbeatData } from "./aurora-postgres-heartbeat-data-schema.js"; export type { AwsArtifactRegistryImportData } from "./aws-artifact-registry-import-data-schema.js"; +export type { AwsBedrockAiHeartbeatData } from "./aws-bedrock-ai-heartbeat-data-schema.js"; export type { AwsBuildImportData } from "./aws-build-import-data-schema.js"; export type { AwsCodeBuildHeartbeatData } from "./aws-code-build-heartbeat-data-schema.js"; export type { AwsComputeClusterHeartbeatData } from "./aws-compute-cluster-heartbeat-data-schema.js"; @@ -61,6 +66,7 @@ export type { AzureCustomCertificateConfig } from "./azure-custom-certificate-co export type { AzureDaemonHeartbeatData } from "./azure-daemon-heartbeat-data-schema.js"; export type { AzureFlexibleServerPostgresHeartbeatData } from "./azure-flexible-server-postgres-heartbeat-data-schema.js"; export type { AzureFlexibleServerPostgresImportData } from "./azure-flexible-server-postgres-import-data-schema.js"; +export type { AzureFoundryAiHeartbeatData } from "./azure-foundry-ai-heartbeat-data-schema.js"; export type { AzureKeyVaultHeartbeatData } from "./azure-key-vault-heartbeat-data-schema.js"; export type { AzureKvImportData } from "./azure-kv-import-data-schema.js"; export type { AzureManagedIdentityServiceAccountHeartbeatData } from "./azure-managed-identity-service-account-heartbeat-data-schema.js"; @@ -155,7 +161,10 @@ export type { Envelope } from "./envelope-schema.js"; export type { EventChange } from "./event-change-schema.js"; export type { EventState } from "./event-state-schema.js"; export type { ExposeProtocol } from "./expose-protocol-schema.js"; +export type { ExternalAiHeartbeatData } from "./external-ai-heartbeat-data-schema.js"; export type { FailureDomainSelection } from "./failure-domain-selection-schema.js"; +export type { FinetuneMethod } from "./finetune-method-schema.js"; +export type { FinetuneSpec } from "./finetune-spec-schema.js"; export type { GcpArtifactRegistryHeartbeatData } from "./gcp-artifact-registry-heartbeat-data-schema.js"; export type { GcpArtifactRegistryImportData } from "./gcp-artifact-registry-import-data-schema.js"; export type { GcpBuildImportData } from "./gcp-build-import-data-schema.js"; @@ -185,6 +194,7 @@ export type { GcpServiceActivationImportData } from "./gcp-service-activation-im export type { GcpServiceUsageActivationHeartbeatData } from "./gcp-service-usage-activation-heartbeat-data-schema.js"; export type { GcpStorageImportData } from "./gcp-storage-import-data-schema.js"; export type { GcpVaultImportData } from "./gcp-vault-import-data-schema.js"; +export type { GcpVertexAiHeartbeatData } from "./gcp-vertex-ai-heartbeat-data-schema.js"; export type { GcpVpcNetworkHeartbeatData } from "./gcp-vpc-network-heartbeat-data-schema.js"; export type { GcpWorkerImportData } from "./gcp-worker-import-data-schema.js"; export type { GpuSpec } from "./gpu-spec-schema.js"; @@ -371,6 +381,10 @@ export type { WorkerTrigger } from "./worker-trigger-schema.js"; export type { WorkloadHeartbeatStatus } from "./workload-heartbeat-status-schema.js"; export type { WorkloadReplicaStatus } from "./workload-replica-status-schema.js"; export { AgentStatusSchema } from "./agent-status-schema.js"; +export { AiHeartbeatDataSchema } from "./ai-heartbeat-data-schema.js"; +export { AiHeartbeatStatusSchema } from "./ai-heartbeat-status-schema.js"; +export { AiOutputsSchema } from "./ai-outputs-schema.js"; +export { AiSchema } from "./ai-schema.js"; export { AlienErrorSchema } from "./alien-error-schema.js"; export { AlienEventSchema } from "./alien-event-schema.js"; export { ArchitectureSchema } from "./architecture-schema.js"; @@ -380,6 +394,7 @@ export { ArtifactRegistryOutputsSchema } from "./artifact-registry-outputs-schem export { ArtifactRegistrySchema } from "./artifact-registry-schema.js"; export { AuroraPostgresHeartbeatDataSchema } from "./aurora-postgres-heartbeat-data-schema.js"; export { AwsArtifactRegistryImportDataSchema } from "./aws-artifact-registry-import-data-schema.js"; +export { AwsBedrockAiHeartbeatDataSchema } from "./aws-bedrock-ai-heartbeat-data-schema.js"; export { AwsBuildImportDataSchema } from "./aws-build-import-data-schema.js"; export { AwsCodeBuildHeartbeatDataSchema } from "./aws-code-build-heartbeat-data-schema.js"; export { AwsComputeClusterHeartbeatDataSchema } from "./aws-compute-cluster-heartbeat-data-schema.js"; @@ -433,6 +448,7 @@ export { AzureCustomCertificateConfigSchema } from "./azure-custom-certificate-c export { AzureDaemonHeartbeatDataSchema } from "./azure-daemon-heartbeat-data-schema.js"; export { AzureFlexibleServerPostgresHeartbeatDataSchema } from "./azure-flexible-server-postgres-heartbeat-data-schema.js"; export { AzureFlexibleServerPostgresImportDataSchema } from "./azure-flexible-server-postgres-import-data-schema.js"; +export { AzureFoundryAiHeartbeatDataSchema } from "./azure-foundry-ai-heartbeat-data-schema.js"; export { AzureKeyVaultHeartbeatDataSchema } from "./azure-key-vault-heartbeat-data-schema.js"; export { AzureKvImportDataSchema } from "./azure-kv-import-data-schema.js"; export { AzureManagedIdentityServiceAccountHeartbeatDataSchema } from "./azure-managed-identity-service-account-heartbeat-data-schema.js"; @@ -527,7 +543,10 @@ export { EnvelopeSchema } from "./envelope-schema.js"; export { EventChangeSchema } from "./event-change-schema.js"; export { EventStateSchema } from "./event-state-schema.js"; export { ExposeProtocolSchema } from "./expose-protocol-schema.js"; +export { ExternalAiHeartbeatDataSchema } from "./external-ai-heartbeat-data-schema.js"; export { FailureDomainSelectionSchema } from "./failure-domain-selection-schema.js"; +export { FinetuneMethodSchema } from "./finetune-method-schema.js"; +export { FinetuneSpecSchema } from "./finetune-spec-schema.js"; export { GcpArtifactRegistryHeartbeatDataSchema } from "./gcp-artifact-registry-heartbeat-data-schema.js"; export { GcpArtifactRegistryImportDataSchema } from "./gcp-artifact-registry-import-data-schema.js"; export { GcpBuildImportDataSchema } from "./gcp-build-import-data-schema.js"; @@ -557,6 +576,7 @@ export { GcpServiceActivationImportDataSchema } from "./gcp-service-activation-i export { GcpServiceUsageActivationHeartbeatDataSchema } from "./gcp-service-usage-activation-heartbeat-data-schema.js"; export { GcpStorageImportDataSchema } from "./gcp-storage-import-data-schema.js"; export { GcpVaultImportDataSchema } from "./gcp-vault-import-data-schema.js"; +export { GcpVertexAiHeartbeatDataSchema } from "./gcp-vertex-ai-heartbeat-data-schema.js"; export { GcpVpcNetworkHeartbeatDataSchema } from "./gcp-vpc-network-heartbeat-data-schema.js"; export { GcpWorkerImportDataSchema } from "./gcp-worker-import-data-schema.js"; export { GpuSpecSchema } from "./gpu-spec-schema.js"; diff --git a/packages/core/src/generated/zod/resource-heartbeat-data-schema.ts b/packages/core/src/generated/zod/resource-heartbeat-data-schema.ts index d34dbdda9..af19316e6 100644 --- a/packages/core/src/generated/zod/resource-heartbeat-data-schema.ts +++ b/packages/core/src/generated/zod/resource-heartbeat-data-schema.ts @@ -4,6 +4,7 @@ */ import * as z from "zod"; +import { AiHeartbeatDataSchema } from "./ai-heartbeat-data-schema.js"; import { ArtifactRegistryHeartbeatDataSchema } from "./artifact-registry-heartbeat-data-schema.js"; import { AzureContainerAppsEnvironmentHeartbeatDataSchema } from "./azure-container-apps-environment-heartbeat-data-schema.js"; import { AzureResourceGroupHeartbeatDataSchema } from "./azure-resource-group-heartbeat-data-schema.js"; @@ -125,6 +126,11 @@ export const ResourceHeartbeatDataSchema = z.union([z.object({ return AzureServiceBusNamespaceHeartbeatDataSchema }, "resourceType": z.enum(["azure_service_bus_namespace"]) + }), z.object({ + get "data"(){ + return AiHeartbeatDataSchema + }, +"resourceType": z.enum(["ai"]) })]) export type ResourceHeartbeatData = z.infer \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 59ccc42e9..02eb9b25d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,6 +9,7 @@ export * from "./artifact-registry.js" export * from "./vault.js" export * from "./kv.js" export * from "./postgres.js" +export * from "./ai.js" export * from "./queue.js" export * from "./email.js" export * from "./service-account.js" diff --git a/packages/package-layout/fixture/src/compile-entry.ts b/packages/package-layout/fixture/src/compile-entry.ts index e8147836c..b57726af0 100644 --- a/packages/package-layout/fixture/src/compile-entry.ts +++ b/packages/package-layout/fixture/src/compile-entry.ts @@ -1,26 +1,42 @@ /** * Static-embed entry point for the `bun build --compile` check in `run.ts`. * - * It imports `@alienplatform/bindings` through the pinned `./native` subpath — - * the statically analyzable specifier that lets the Bun compiler stage the - * platform `.node` addon into a single-file binary (bindings PACKAGE_LAYOUT.md, - * "Exports map"). Unlike `imports.ts`, this uses a STATIC import on purpose: - * `bun build --compile` only follows statically analyzable imports. + * A compiled Worker's generated bootstrap calls `installEmbeddedAddon()` from + * `@alienplatform/sdk/native` once, which registers BOTH bun-embedded native + * addons — `@alienplatform/bindings` (kv/storage/queue/vault) and + * `@alienplatform/ai-gateway` (the `ai()` client) — with their loaders. The SDK's + * re-exported factories then resolve those addons with no filesystem lookup, + * which is the only way native access works inside a single-file binary. * - * `@alienplatform/bindings` ships this specifier, but the per-platform - * `.node` prebuilds are only staged by the release pipeline - * (.github/workflows/release.yml), so in a workspace checkout the packed - * tarball has no addon next to `native.js` and the `bun build --compile` - * step fails. + * These are STATIC imports on purpose: `bun build --compile` only follows + * statically analyzable imports. `@alienplatform/bindings/native` is imported + * directly too, to keep the direct-consumer path covered alongside the SDK path. + * + * The per-platform `.node` prebuilds are only staged by the release pipeline + * (.github/workflows/release.yml) or by `run.ts` here, so in a workspace checkout + * with no staged addon the `bun build --compile` step fails. */ import { storage } from "@alienplatform/bindings/native" +import { ai } from "@alienplatform/sdk" +import { installEmbeddedAddon } from "@alienplatform/sdk/native" + +// Register both embedded addons up front, exactly as a compiled Worker bootstrap +// does. This eagerly loads each `.node` (through each package's `/native` entry), +// so a binary that runs this after the staged `.node` files are removed proves +// both addons are embedded, not filesystem-loaded. +installEmbeddedAddon() -// Reference the import so the compiler must stage the native addon, then exit 0 -// so a successfully compiled binary is observably runnable. -const factory: unknown = storage -if (typeof factory !== "function") { - throw new Error("expected the ./native storage factory to be a function") +// Reference the factories so the compiler must stage the addons: `storage` via the +// direct bindings/native path, `ai` via the SDK's re-export (the Worker path that +// the embedded ai-gateway addon fixes). +for (const [name, factory] of [ + ["bindings storage", storage], + ["sdk ai", ai], +] as const) { + if (typeof factory !== "function") { + throw new Error(`expected the ${name} factory to be a function after installEmbeddedAddon`) + } } -console.log("compile-entry: native binding factory embedded") +console.log("compile-entry: bindings + ai-gateway native addons embedded via the SDK") diff --git a/packages/package-layout/steps/compile.ts b/packages/package-layout/steps/compile.ts index af15a9ea9..7f75c4d2b 100644 --- a/packages/package-layout/steps/compile.ts +++ b/packages/package-layout/steps/compile.ts @@ -1,111 +1,121 @@ -// Step 8 — bun build --compile of the ./native embed entry. +// Step 8 — bun build --compile of the SDK native-embed entry. // -// The `./native` entry (bindings/src/native.ts) imports the addon through -// the literal `./alien-bindings.node` specifier so bun's compiler can stage -// it into the single-file binary — but only if that file is physically -// present next to the installed package's dist/native.js at build time -// (in production `alien build`'s TypeScript toolchain owns that staging, -// see packages/bindings/PACKAGE_LAYOUT.md; here we stage the -// addon `run.ts` itself resolved above). `--format=cjs` is required: a -// plain ESM `bun build --compile` of this entry embeds the addon but -// crashes on load with `ReferenceError: __require is not defined` — see -// this compile-smoke step for the verified repro. +// The entry (fixture/src/compile-entry.ts) calls `installEmbeddedAddon()` from +// `@alienplatform/sdk/native`, which pulls in both `@alienplatform/bindings/native` +// and `@alienplatform/ai-gateway/native`. Each `./native` entry imports its addon +// through a literal specifier (`./alien-bindings.node`, `./alien-ai-gateway.node`) +// so bun's compiler stages it into the single-file binary — but only if that file +// is physically present next to the installed package's dist/native.js at build +// time (in production `alien build`'s TypeScript toolchain owns that staging, see +// PACKAGE_LAYOUT.md; here `run.ts` staged the host addons resolved above). +// `--format=cjs` is required: a plain ESM `bun build --compile` of this entry +// embeds the addons but crashes on load with `ReferenceError: __require is not +// defined` — the verified repro this compile-smoke step guards. import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs" import { dirname, join } from "node:path" import { type CheckResult, type Ctx, lastLine, run } from "./shared.ts" export function compileNativeEmbed(ctx: Ctx): CheckResult[] { - const { fixtureDir, bunAvailable, addonPath } = ctx + const { fixtureDir, bunAvailable, addonPath, aiAddonPath } = ctx if (!bunAvailable) return [] const compiledDir = join(fixtureDir, ".compiled") mkdirSync(compiledDir, { recursive: true }) const outFile = join(compiledDir, "compile-entry-bin") - const stagedAddonPath = join( - fixtureDir, - "node_modules", - "@alienplatform", - "bindings", - "dist", - "alien-bindings.node", - ) - if (!existsSync(dirname(stagedAddonPath))) { - return [ - { - check: "compile", - package: "bindings", - status: "fail", - reason: "installed bindings package is unavailable (see the install failure above)", - evidence: `expected package dist at ${dirname(stagedAddonPath)}`, - }, - ] - } + // Each package's `./native` entry imports its addon through a literal specifier + // next to its own dist/native.js; stage the host addon there for both. + const stages = [ + { + pkg: "bindings", + staged: join( + fixtureDir, + "node_modules", + "@alienplatform", + "bindings", + "dist", + "alien-bindings.node", + ), + addonPath, + }, + { + pkg: "ai-gateway", + staged: join( + fixtureDir, + "node_modules", + "@alienplatform", + "ai-gateway", + "dist", + "alien-ai-gateway.node", + ), + addonPath: aiAddonPath, + }, + ] as const - if (addonPath) { + for (const stage of stages) { + if (!existsSync(dirname(stage.staged))) { + return [ + { + check: "compile", + package: stage.pkg, + status: "fail", + reason: `installed ${stage.pkg} package is unavailable (see the install failure above)`, + evidence: `expected package dist at ${dirname(stage.staged)}`, + }, + ] + } + if (!stage.addonPath) { + return [ + { + check: "compile", + package: stage.pkg, + status: "fail", + reason: `no ${stage.pkg} addon available to stage (see the addon-build failure above)`, + evidence: `expected addon at ${stage.staged}`, + }, + ] + } try { - copyFileSync(addonPath, stagedAddonPath) + copyFileSync(stage.addonPath, stage.staged) } catch (error) { return [ { check: "compile", - package: "bindings", + package: stage.pkg, status: "fail", - reason: "failed to stage the host addon for bun build --compile", + reason: `failed to stage the host ${stage.pkg} addon for bun build --compile`, evidence: error instanceof Error ? error.message : String(error), }, ] } } - const built = addonPath - ? run( - "bun", - [ - "build", - "--compile", - "--format=cjs", - join("src", "compile-entry.ts"), - "--outfile", - outFile, - ], - fixtureDir, - ) - : undefined - - if (!built) { - return [ - { - check: "compile", - package: "bindings", - status: "fail", - reason: "no addon available to stage (see the addon-build failure above)", - evidence: `expected addon at ${stagedAddonPath}`, - }, - ] - } + const built = run( + "bun", + ["build", "--compile", "--format=cjs", join("src", "compile-entry.ts"), "--outfile", outFile], + fixtureDir, + ) if (built.status !== 0) { return [ { check: "compile", - package: "bindings", + package: "sdk-native", status: "fail", - reason: "bun build --compile of ./native entry fails with the addon staged", + reason: "bun build --compile of the SDK native entry fails with the addons staged", evidence: lastLine(built.stderr) || lastLine(built.stdout) || `exit ${built.status}`, }, ] } - // Remove the staged .node now: if the binary didn't truly embed it, - // running with the source file gone proves that (mirrors - // this compile-smoke step). - rmSync(stagedAddonPath, { force: true }) + // Remove BOTH staged .node files: if the binary didn't truly embed them, + // running with the source files gone proves that. + for (const stage of stages) rmSync(stage.staged, { force: true }) const ran = run(outFile, [], fixtureDir) return [ { check: "compile", - package: "bindings", + package: "sdk-native", status: ran.status === 0 ? "pass" : "fail", reason: ran.status === 0 ? "ok" : "compiled binary exited non-zero", evidence: ran.status === 0 ? lastLine(ran.stdout) : lastLine(ran.stderr), diff --git a/packages/package-layout/steps/ensure-addon.ts b/packages/package-layout/steps/ensure-addon.ts index 8645df6a9..3e6684241 100644 --- a/packages/package-layout/steps/ensure-addon.ts +++ b/packages/package-layout/steps/ensure-addon.ts @@ -1,88 +1,96 @@ -// Step 3.5 — ensure a native addon exists for the host platform. +// Step 3.5 — ensure native addons exist for the host platform. // -// The loader (packages/bindings/src/loader.ts) resolves the addon in order: -// an ALIEN_BINDINGS_ADDON_PATH override, the per-platform prebuild package -// (optionalDependencies — only injected at publish time by `napi -// prepublish` in the release pipeline (.github/workflows/release.yml); -// never present when packing straight from workspace -// source), then a locally-built dev `.node` found by walking up from the -// installed package looking for crates/alien-bindings-node. On a developer -// machine that walk reaches this repo's real crates/alien-bindings-node and -// finds the `.node` a developer built earlier, so the fixture passes -// without any help here. CI has neither: no prebuild (04a ships those) and -// no dev `.node` (gitignored, built per-machine) — so build one ourselves -// whenever nothing else would resolve, and hand its path to every -// subprocess via the override env var. Skipped (and logged) whenever an -// addon is already available, so local runs stay fast. +// Each loader (packages/{bindings,ai-gateway}/src/loader.ts) resolves its addon +// in order: an ALIEN_*_ADDON_PATH override, the per-platform prebuild package +// (optionalDependencies — only injected at publish time by the release pipeline +// (.github/workflows/release.yml); never present when packing straight from +// workspace source), then a locally-built dev `.node` found by walking up from +// the installed package to crates/. On a developer machine that walk +// finds a `.node` built earlier, so the fixture passes without help here. CI has +// neither: no prebuild and no dev `.node` (gitignored, built per-machine) — so +// build one ourselves whenever nothing else would resolve, and hand its path to +// the compile step for staging. Skipped (and logged) whenever an addon is +// already available, so local runs stay fast. import { existsSync, readFileSync } from "node:fs" import { createRequire } from "node:module" import { dirname, join, relative } from "node:path" -import { fileURLToPath } from "node:url" // The napi triple mapping is owned by the bindings loader; reuse it here rather -// than keeping a second copy (compile-smoke.ts imports it the same way). +// than keeping a second copy (compile-smoke.ts imports it the same way). Both +// addons share the same triple mapping. import { platformTriple } from "../../bindings/src/loader.ts" import { type CheckResult, type Ctx, lastLine, run } from "./shared.ts" +/** Resolve the napi CLI bin from the workspace install (avoids `npx` reaching the registry). */ +function napiBinPath(): string { + const napiRequire = createRequire(import.meta.url) + const napiPkgPath = napiRequire.resolve("@napi-rs/cli/package.json") + const napiPkg = JSON.parse(readFileSync(napiPkgPath, "utf8")) as { + bin: string | Record + } + const napiBinRel = typeof napiPkg.bin === "string" ? napiPkg.bin : napiPkg.bin.napi + return join(dirname(napiPkgPath), napiBinRel) +} + export function ensureAddon(ctx: Ctx): CheckResult[] { const { scriptDir, fixtureDir, repoRoot } = ctx const results: CheckResult[] = [] - - const bindingsNodeDir = join(repoRoot, "crates", "alien-bindings-node") const triple = platformTriple() - const devAddonPath = join(bindingsNodeDir, `alien-bindings-node.${triple}.node`) - const prebuildInstalledDir = join( - fixtureDir, - "node_modules", - "@alienplatform", - `bindings-${triple}`, - ) - if (existsSync(prebuildInstalledDir)) { - console.log( - `[addon] per-platform prebuild package installed for '${triple}' — no source build needed.`, - ) - } else if (existsSync(devAddonPath)) { - ctx.addonPath = devAddonPath - console.log( - `[addon] using existing dev addon at ${relative(scriptDir, devAddonPath)} (fast path, no build).`, + /** + * Resolve (or build) one package's host dev addon. Returns the dev-addon path + * to stage, or `undefined` when a prebuild is already installed (it resolves + * via node_modules) or a build fails (a failure is pushed to `results`). + */ + function resolveAddon(pkgName: string, crateName: string): string | undefined { + const crateDir = join(repoRoot, "crates", crateName) + const devAddonPath = join(crateDir, `${crateName}.${triple}.node`) + const prebuildInstalledDir = join( + fixtureDir, + "node_modules", + "@alienplatform", + `${pkgName}-${triple}`, ) - } else { + + if (existsSync(prebuildInstalledDir)) { + console.log( + `[addon] per-platform ${pkgName} prebuild installed for '${triple}' — no source build needed.`, + ) + return undefined + } + if (existsSync(devAddonPath)) { + console.log( + `[addon] using existing ${crateName} dev addon at ${relative(scriptDir, devAddonPath)} (fast path, no build).`, + ) + return devAddonPath + } console.log( - `[addon] no prebuild and no dev addon for '${triple}' — building one with \`napi build --platform --release\` in crates/alien-bindings-node (CI path)...`, + `[addon] no prebuild and no dev addon for ${crateName} '${triple}' — building one with \`napi build --platform --release\` in crates/${crateName} (CI path)...`, ) - // Resolve the napi CLI from the workspace install and spawn its bin - // directly with cwd set to the translation crate. This avoids `npx` - // falling through to the registry when a local install is incomplete. - const napiRequire = createRequire(import.meta.url) - const napiPkgPath = napiRequire.resolve("@napi-rs/cli/package.json") - const napiPkg = JSON.parse(readFileSync(napiPkgPath, "utf8")) as { - bin: string | Record - } - const napiBinRel = typeof napiPkg.bin === "string" ? napiPkg.bin : napiPkg.bin.napi - const napiBinPath = join(dirname(napiPkgPath), napiBinRel) const build = run( process.execPath, - [napiBinPath, "build", "--platform", "--release"], - bindingsNodeDir, + [napiBinPath(), "build", "--platform", "--release"], + crateDir, ) if (build.status === 0 && existsSync(devAddonPath)) { - ctx.addonPath = devAddonPath console.log(`[addon] built ${relative(scriptDir, devAddonPath)}.`) - } else { - console.error( - "[addon] source build failed; the runtime/compile checks below will fail to load the addon.", - ) - results.push({ - check: "addon-build", - package: "bindings", - status: "fail", - reason: "napi build --platform --release did not produce a .node for this host", - evidence: lastLine(build.stderr) || lastLine(build.stdout) || `exit ${build.status}`, - }) + return devAddonPath } + console.error( + `[addon] ${crateName} source build failed; the runtime/compile checks below will fail to load the addon.`, + ) + results.push({ + check: "addon-build", + package: pkgName, + status: "fail", + reason: `napi build --platform --release did not produce a .node for ${crateName} on this host`, + evidence: lastLine(build.stderr) || lastLine(build.stdout) || `exit ${build.status}`, + }) + return undefined } + ctx.addonPath = resolveAddon("bindings", "alien-bindings-node") + ctx.aiAddonPath = resolveAddon("ai-gateway", "alien-ai-gateway-node") ctx.addonEnv = ctx.addonPath ? { ALIEN_BINDINGS_ADDON_PATH: ctx.addonPath } : undefined return results diff --git a/packages/package-layout/steps/pack-packages.ts b/packages/package-layout/steps/pack-packages.ts index 38a70cae6..260f8cc92 100644 --- a/packages/package-layout/steps/pack-packages.ts +++ b/packages/package-layout/steps/pack-packages.ts @@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs" import { join, relative } from "node:path" import { type CheckResult, type Ctx, lastLine, run } from "./shared.ts" -const PACK_TARGETS = ["sdk", "core", "bindings", "commands"] as const +const PACK_TARGETS = ["sdk", "core", "bindings", "commands", "ai-gateway"] as const export function packPackages(ctx: Ctx): CheckResult[] { const { scriptDir, packagesDir, tarballsDir, tarballs } = ctx diff --git a/packages/package-layout/steps/shared.ts b/packages/package-layout/steps/shared.ts index ea4a2b366..1e15a2760 100644 --- a/packages/package-layout/steps/shared.ts +++ b/packages/package-layout/steps/shared.ts @@ -41,8 +41,10 @@ export interface Ctx { bunAvailable: boolean /** name -> absolute tarball path, for packages that packed successfully. */ tarballs: Map - /** Resolved dev-addon path, when one had to be located or built for this host. */ + /** Resolved bindings dev-addon path, when one had to be located or built for this host. */ addonPath?: string + /** Resolved ai-gateway dev-addon path, when one is available for this host. */ + aiAddonPath?: string /** Env carrying ALIEN_BINDINGS_ADDON_PATH for subprocesses, when `addonPath` is set. */ addonEnv?: NodeJS.ProcessEnv } diff --git a/packages/package-layout/steps/write-manifest.ts b/packages/package-layout/steps/write-manifest.ts index 7bcf642c5..dcac9eef3 100644 --- a/packages/package-layout/steps/write-manifest.ts +++ b/packages/package-layout/steps/write-manifest.ts @@ -9,8 +9,8 @@ export function writeManifest(ctx: Ctx): CheckResult[] { // Direct dependencies the consumer imports; overrides pin every transitive // @alienplatform/* to a packed tarball so npm never reaches the registry for one. - const DIRECT_DEP_PACKAGES = ["sdk", "bindings", "commands"] as const - const OVERRIDE_PACKAGES = ["core", "sdk", "bindings", "commands"] as const + const DIRECT_DEP_PACKAGES = ["sdk", "bindings", "commands", "ai-gateway"] as const + const OVERRIDE_PACKAGES = ["core", "sdk", "bindings", "commands", "ai-gateway"] as const function fileSpec(tarball: string): string { return `file:${relative(fixtureDir, tarball).split("\\").join("/")}` diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d87d3f827..793e41c2d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -31,7 +31,7 @@ "author": "Alien Software, Inc. ", "license": "FSL-1.1-Apache-2.0", "homepage": "https://alien.dev", - "description": "TypeScript SDK for Alien Worker apps — handler APIs and re-exported binding factories", + "description": "TypeScript SDK for Alien Worker apps \u2014 handler APIs and re-exported binding factories", "repository": { "type": "git", "url": "https://github.com/alienplatform/alien.git", @@ -50,6 +50,7 @@ "vitest": "^3.1.4" }, "dependencies": { + "@alienplatform/ai-gateway": "workspace:^", "@alienplatform/bindings": "workspace:^", "@alienplatform/core": "workspace:^", "@bufbuild/protobuf": "^2.5.3", diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 1937311b2..e1231f1fc 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -58,9 +58,33 @@ export type { export { storage, kv, queue, vault, container } from "@alienplatform/bindings" export type { Storage, Kv, Queue, Vault, Container } from "@alienplatform/bindings" +// ============================================================================ +// AI — re-exported from @alienplatform/ai-gateway (the in-process Rust gateway) +// ============================================================================ + +export { + ai, + getAiConnection, + Ai, + isExternalAiBinding, + parseAiBinding, +} from "@alienplatform/ai-gateway" +export type { + AiBinding, + AmbientAiBinding, + ExternalAiBinding, + AiConnection, + AiModel, + ChatCompletionCreateParams, + FinetuneJobStatus, + FinetuneResult, + ResponseCreateParams, +} from "@alienplatform/ai-gateway" + // ============================================================================ // Errors — re-exported from @alienplatform/bindings and @alienplatform/core // ============================================================================ export { BindingNotConfiguredError } from "@alienplatform/bindings" -export { AlienError } from "@alienplatform/core" +export { AiTransportError, AiUpstreamError } from "@alienplatform/ai-gateway" +export { AlienError, BindingNotFoundError, InvalidBindingConfigError } from "@alienplatform/core" diff --git a/packages/sdk/src/native.ts b/packages/sdk/src/native.ts index a37b4e6d6..211f69495 100644 --- a/packages/sdk/src/native.ts +++ b/packages/sdk/src/native.ts @@ -1,26 +1,33 @@ /** * `@alienplatform/sdk/native` — the embedded-addon bridge for compiled Workers. * - * A Worker depends only on `@alienplatform/sdk`; `@alienplatform/bindings` is a - * transitive dependency reached *through* the SDK, so a Worker's own - * `node_modules` cannot resolve `@alienplatform/bindings/native` directly. But - * it can resolve the SDK, and the SDK can resolve `@alienplatform/bindings/native` - * (bindings is the SDK's direct dependency). This subpath is that one hop: it - * re-exports `installEmbeddedAddon` so a compiled Worker bootstrap can register - * the bun-embedded addon with the bindings loader without naming the bindings - * package it can't see. + * A Worker depends only on `@alienplatform/sdk`; `@alienplatform/bindings` and + * `@alienplatform/ai-gateway` are transitive dependencies reached *through* the + * SDK, so a Worker's own `node_modules` cannot resolve their `/native` subpaths + * directly. But it can resolve the SDK, and the SDK can resolve both (each is a + * direct dependency). This subpath is that one hop: it installs both + * bun-embedded addons so a compiled Worker bootstrap can register them without + * naming packages it can't see. * - * `bun build --compile` follows this static re-export chain - * (`@alienplatform/sdk/native` → `@alienplatform/bindings/native` → the staged - * `alien-bindings.node`) and embeds the addon into the single-file binary. The - * SDK keeps `@alienplatform/bindings` external (see tsdown.config.ts), so the - * compiled binary has exactly one bindings module — the same one the SDK's - * re-exported `kv`/`storage`/`queue`/`vault` resolve through — and the - * registration performed here is visible to those factories. + * `bun build --compile` follows the static re-export chains + * (`@alienplatform/sdk/native` → `@alienplatform/{bindings,ai-gateway}/native` → + * the staged `alien-bindings.node` / `alien-ai-gateway.node`) and embeds both + * addons into the single-file binary. The SDK keeps both packages external (see + * tsdown.config.ts), so the compiled binary has exactly one module for each — + * the same ones the SDK's re-exported `kv`/`storage`/`queue`/`vault` and + * `ai`/`getAiConnection` resolve through — so the registrations here are visible + * to those factories. * - * This module is imported only by a generated compiled entry (never in dev), - * so its transitive static `import addon from "./alien-bindings.node"` inside - * `@alienplatform/bindings/native` only runs where that addon has been staged. + * This module is imported only by a generated compiled entry (never in dev), so + * the transitive static `import addon from "./alien-*.node"` inside each + * `/native` only runs where that addon has been staged. */ -export { installEmbeddedAddon } from "@alienplatform/bindings/native" +import { installEmbeddedAddon as installAiGatewayAddon } from "@alienplatform/ai-gateway/native" +import { installEmbeddedAddon as installBindingsAddon } from "@alienplatform/bindings/native" + +/** Register both bun-embedded addons (bindings + ai-gateway) with their default loaders. */ +export function installEmbeddedAddon(): void { + installBindingsAddon() + installAiGatewayAddon() +} diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 548f91d75..a9e44848b 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -14,14 +14,21 @@ export default defineConfig({ // This allows bun build --compile to work without needing // transitive dependencies installed in the user's project. // - // Exception: @alienplatform/bindings must stay external. It holds the native - // addon loader's process-level state (the embedded-addon registration used by - // compiled binaries). Inlining a copy here would give the SDK its own loader - // instance, so a compiled worker's `installEmbeddedAddon()` (which registers - // into the real bindings module via `@alienplatform/bindings/native`) would - // not be seen by the SDK's re-exported `kv`/`storage`. Keeping it external - // makes the compiled binary share one bindings module. It's a real runtime - // dependency, so the package stays self-contained. - external: ["@alienplatform/bindings", "@alienplatform/bindings/native"], + // Exception: @alienplatform/bindings and @alienplatform/ai-gateway must stay + // external. Each holds a native addon loader's process-level state (the + // embedded-addon registration used by compiled binaries). Inlining a copy here + // would give the SDK its own loader instance, so a compiled worker's + // `installEmbeddedAddon()` (which registers into the real modules via their + // `/native` subpaths) would not be seen by the SDK's re-exported + // `kv`/`storage`/`queue`/`vault` (bindings) or `ai`/`getAiConnection` + // (ai-gateway). Keeping them external makes the compiled binary share one + // module for each. They're real runtime dependencies, so the package stays + // self-contained. + external: [ + "@alienplatform/bindings", + "@alienplatform/bindings/native", + "@alienplatform/ai-gateway", + "@alienplatform/ai-gateway/native", + ], noExternal: [/.*/], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 629e5ea69..7e8281dcd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,37 @@ importers: specifier: ^3.0.0 version: 3.7.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.10.4) + packages/ai-gateway: + dependencies: + '@alienplatform/core': + specifier: link:../core + version: link:../core + zod: + specifier: 4.3.2 + version: 4.3.2 + devDependencies: + '@alienplatform/typescript-config': + specifier: link:../config-typescript + version: link:../config-typescript + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@napi-rs/cli': + specifier: ^3.0.0 + version: 3.7.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.10.4) + '@types/node': + specifier: ^24.0.15 + version: 24.10.4 + tsdown: + specifier: ^0.13.0 + version: 0.13.5(typescript@5.8.3) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.1.4 + version: 3.2.7(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(tsx@4.21.0) + packages/alien-cli-npm: {} packages/bindings: @@ -200,6 +231,9 @@ importers: packages/sdk: dependencies: + '@alienplatform/ai-gateway': + specifier: workspace:^ + version: link:../ai-gateway '@alienplatform/bindings': specifier: workspace:^ version: link:../bindings diff --git a/tests/e2e/test-apps/comprehensive-typescript/alien.ts b/tests/e2e/test-apps/comprehensive-typescript/alien.ts index d0f7a1098..2d202c49b 100644 --- a/tests/e2e/test-apps/comprehensive-typescript/alien.ts +++ b/tests/e2e/test-apps/comprehensive-typescript/alien.ts @@ -14,6 +14,7 @@ const queue = new alien.Queue("alien-queue").build() // race that consumer. This queue has exactly one consumer: the queue trigger. const eventsQueue = new alien.Queue("alien-events-queue").build() const postgres = isLocal ? new alien.Postgres("alien-postgres").build() : undefined +const ai = new alien.Ai("test-ai").build() let workerBuilder = new alien.Worker("alien-ts-worker") .code({ @@ -35,6 +36,7 @@ let workerBuilder = new alien.Worker("alien-ts-worker") .link(kv) .link(queue) .link(eventsQueue) + .link(ai) .commandsEnabled(true) // Event triggers under test: each one must invoke the registered handler in // src/index.ts, which records the event in `alien-kv` for read-back. @@ -56,6 +58,7 @@ const executionPermissions = [ "queue/data-read", "queue/data-write", "worker/execute", + "ai/invoke", ] if (postgres) { executionPermissions.push("postgres/data-access") @@ -74,6 +77,7 @@ let stackBuilder = new alien.Stack("alien-ts-stack") .add(kv, "frozen") .add(queue, "frozen") .add(eventsQueue, "frozen") + .add(ai, "frozen") if (postgres) { stackBuilder = stackBuilder.add(postgres, "live") } diff --git a/tests/e2e/test-apps/comprehensive-typescript/src/handlers/ai.ts b/tests/e2e/test-apps/comprehensive-typescript/src/handlers/ai.ts new file mode 100644 index 000000000..0aca18ff1 --- /dev/null +++ b/tests/e2e/test-apps/comprehensive-typescript/src/handlers/ai.ts @@ -0,0 +1,65 @@ +import { AlienError } from "@alienplatform/core" +import { ai, parseAiBinding } from "@alienplatform/sdk" +import { Hono } from "hono" + +const app = new Hono() + +// GET /ai-test +// +// Proves that the runtime injected ALIEN_TEST_AI_BINDING and that the binding +// parses to a well-formed config. With `?invoke=1` it additionally lists the +// cloud's model catalog and makes a real one-line chat call through the +// embedded gateway — the full app -> gateway -> cloud LLM path under the +// workload's ambient credentials. +app.get("/ai-test", async c => { + try { + const binding = ai("test-ai") + const config = await parseAiBinding("test-ai") + if (!config) { + return c.json({ injected: false, error: "ALIEN_TEST_AI_BINDING is not set" }, 500) + } + + // Each ambient service names its scope differently; surface one locator string + // so the e2e can assert the controller filled it in, whatever the cloud. + const fields = config as Record + const locator = + config.service === "bedrock" + ? fields.region + : config.service === "vertex" + ? `${fields.project}/${fields.location}` + : config.service === "foundry" + ? fields.endpoint + : undefined + + if (c.req.query("invoke") !== "1") { + return c.json({ injected: true, service: config.service, locator }) + } + + const models = await binding.getAvailableModels() + const model = models[0]?.id + if (!model) { + return c.json( + { injected: true, service: config.service, locator, error: "model catalog is empty" }, + 500, + ) + } + const completion = (await binding.chat.completions.create({ + model, + messages: [{ role: "user", content: "Reply with exactly one word: pong" }], + })) as { choices?: Array<{ message?: { content?: string } }> } + const reply = completion.choices?.[0]?.message?.content ?? "" + return c.json({ + injected: true, + service: config.service, + locator, + modelCount: models.length, + model, + reply, + }) + } catch (error) { + const alienErr = error instanceof AlienError ? error.toExternal() : { message: String(error) } + return c.json({ injected: false, error: alienErr }, 500) + } +}) + +export default app diff --git a/tests/e2e/test-apps/comprehensive-typescript/src/index.ts b/tests/e2e/test-apps/comprehensive-typescript/src/index.ts index ef3df7731..139dba8d5 100644 --- a/tests/e2e/test-apps/comprehensive-typescript/src/index.ts +++ b/tests/e2e/test-apps/comprehensive-typescript/src/index.ts @@ -11,6 +11,7 @@ import { Hono } from "hono" import { sanitizeKvKeyPart } from "./helpers.js" +import aiRoutes from "./handlers/ai.js" import environmentRoutes from "./handlers/environment.js" import eventsRoutes from "./handlers/events.js" import healthRoutes from "./handlers/health.js" @@ -26,6 +27,7 @@ import waitUntilRoutes from "./handlers/wait-until.js" const app = new Hono() // Mount handler routes +app.route("/", aiRoutes) app.route("/", healthRoutes) app.route("/", environmentRoutes) app.route("/", inspectRoutes)