diff --git a/.cargo/config.toml b/.cargo/config.toml index ea8730381..d2af0461a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,5 +2,14 @@ rustflags = ["--cfg", "tokio_unstable"] rustdocflags = ["--cfg", "tokio_unstable"] +[env] +# The deployment reconcile polls a deep async chain on one stack (executor -> resource +# controllers -> the cloud SDKs' tower/hyper/rustls futures). In debug builds those frames +# are un-inlined and the chain needs ~3MB, over tokio's 2MB default thread-stack, so the +# alien-test cloud e2e (push_/pull_) overflows its test thread. Release binaries are +# unaffected (inlined frames fit 2MB), so this only sizes dev/test threads. 8MB matches the +# headroom alien-worker-runtime already gives its runtime for the same reason. +RUST_MIN_STACK = "8388608" + [target.wasm32-unknown-unknown] rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/.github/workflows/ci-fast.yml b/.github/workflows/ci-fast.yml index 021536e56..c59c4d63e 100644 --- a/.github/workflows/ci-fast.yml +++ b/.github/workflows/ci-fast.yml @@ -159,13 +159,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: @@ -177,7 +178,7 @@ jobs: --exclude byocdb \ --exclude endpoint-agent \ --exclude alien-test-server \ - --filter-expr 'not (package(alien-aws-clients) and kind(test)) and not (package(alien-gcp-clients) and kind(test)) and not (package(alien-azure-clients) and kind(test)) and not (package(alien-bindings) and kind(test)) and not binary_id(alien-build::build_integration_tests) and not binary_id(alien-build::image_shape_tests) and not binary_id(alien-build::rust_integration_tests) and not binary_id(alien-build::typescript_integration_tests) and not binary_id(alien-worker-runtime::cloudrun) and not binary_id(alien-worker-runtime::lambda) and not binary_id(alien-test::distribution) and not binary_id(alien-test::pull) and not binary_id(alien-test::push) and not binary_id(alien-manager::registry_proxy_cloud_test)' + --filter-expr 'not (package(alien-aws-clients) and kind(test)) and not (package(alien-gcp-clients) and kind(test)) and not (package(alien-azure-clients) and kind(test)) and not (package(alien-bindings) and kind(test)) and not binary_id(alien-build::build_integration_tests) and not binary_id(alien-build::image_shape_tests) and not binary_id(alien-build::rust_integration_tests) and not binary_id(alien-build::typescript_integration_tests) and not binary_id(alien-worker-runtime::cloudrun) and not binary_id(alien-worker-runtime::lambda) and not binary_id(alien-test::distribution) and not binary_id(alien-test::pull) and not binary_id(alien-test::push) and not binary_id(alien-manager::registry_proxy_cloud_test) and not binary_id(alien-ai-gateway::live_bedrock) and not binary_id(alien-ai-gateway::live_foundry_claude) and not binary_id(alien-ai-gateway::live_vertex_claude)' - name: Build all CLIs if: steps.changes.outcome != 'success' || steps.changes.outputs.examples == 'true' || github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fb32e198..5f288b2aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,8 @@ 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; do node -e " const fs = require('fs'); const path = '${pkg}/package.json'; @@ -139,6 +140,7 @@ jobs: packages/bindings/npm/darwin-x64/package.json packages/bindings/npm/linux-x64-gnu/package.json packages/bindings/npm/linux-arm64-gnu/package.json + packages/ai-gateway/package.json - name: Commit and tag if: ${{ !inputs.dry_run }} @@ -151,7 +153,8 @@ 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 git commit -m "chore: release v${VERSION}" git tag "v${VERSION}" git push @@ -356,26 +359,38 @@ jobs: fail-fast: false matrix: include: + # gateway_target/gateway_artifact drive the alien-ai-gateway launcher + # binary built alongside the addon: darwin legs share the addon's + # target; linux legs build it statically against musl, so one binary + # (per arch) serves both the glibc and Alpine prebuild packages. - runner: depot-macos-15 os: macos target: aarch64-apple-darwin triple: darwin-arm64 expect_arch: arm64 + gateway_target: aarch64-apple-darwin + gateway_artifact: darwin-arm64 - runner: depot-macos-15 os: macos target: x86_64-apple-darwin triple: darwin-x64 expect_arch: x86_64 + gateway_target: x86_64-apple-darwin + gateway_artifact: darwin-x64 - runner: depot-ubuntu-24.04-16 os: linux target: x86_64-unknown-linux-gnu triple: linux-x64-gnu expect_arch: x86-64 + gateway_target: x86_64-unknown-linux-musl + gateway_artifact: linux-x64 - runner: depot-ubuntu-24.04-arm-16 os: linux target: aarch64-unknown-linux-gnu triple: linux-arm64-gnu expect_arch: aarch64 + gateway_target: aarch64-unknown-linux-musl + gateway_artifact: linux-arm64 runs-on: ${{ matrix.runner }} timeout-minutes: 60 env: @@ -397,6 +412,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 +480,29 @@ jobs: retention-days: 1 path: packages/bindings/npm/${{ matrix.triple }}/alien-bindings-node.${{ matrix.triple }}.node + # The AI gateway ships as a standalone launcher binary, not an addon. It + # reuses this matrix (same runners/toolchains; it depends on + # alien-bindings' credential resolver, so protoc is already set up). + - name: Add gateway build target (${{ matrix.gateway_target }}) + run: rustup target add ${{ matrix.gateway_target }} + + - name: Build ai-gateway binary (${{ matrix.gateway_artifact }}) + env: + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + run: | + cargo build --release -p alien-ai-gateway --bin alien-ai-gateway \ + --target ${{ matrix.gateway_target }} + + - name: Assert ai-gateway binary architecture + run: file "target/${{ matrix.gateway_target }}/release/alien-ai-gateway" | grep -q "${{ matrix.expect_arch }}" + + - name: Upload ai-gateway binary artifact + uses: actions/upload-artifact@v6 + with: + name: ai-gateway-bin-${{ matrix.gateway_artifact }} + retention-days: 1 + path: target/${{ matrix.gateway_target }}/release/alien-ai-gateway + # 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 +546,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 +609,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 +698,123 @@ 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: inputs.mode == 'stable' && 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@v5 + with: + ref: ${{ inputs.dry_run && github.sha || format('v{0}', needs.prepare.outputs.version) }} + + - name: Apply computed version for dry run + if: ${{ inputs.dry_run }} + uses: actions/download-artifact@v7 + with: + name: release-version-manifests + path: . + + - 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 + + - uses: actions/setup-node@v5 + 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 binaries + uses: actions/download-artifact@v7 + with: + pattern: ai-gateway-bin-* + path: ./gateway-bins + merge-multiple: false + + - name: Generate prebuild manifests + run: node packages/ai-gateway/scripts/generate-prebuilds.mjs + + # The linux binaries are static musl builds, so a triple's gnu and musl + # packages ship the same executable. Artifact download drops the execute + # bit; restore it so npm packs the binary runnable. + - name: Stage binaries into prebuild packages + run: | + for pair in darwin-arm64:darwin-arm64 darwin-x64:darwin-x64 \ + linux-x64-gnu:linux-x64 linux-x64-musl:linux-x64 \ + linux-arm64-gnu:linux-arm64 linux-arm64-musl:linux-arm64; do + triple="${pair%%:*}" + source="${pair##*:}" + cp "./gateway-bins/ai-gateway-bin-${source}/alien-ai-gateway" \ + "packages/ai-gateway/npm/${triple}/alien-ai-gateway" + chmod 755 "packages/ai-gateway/npm/${triple}/alien-ai-gateway" + done + + - name: Build wrapper + run: pnpm --filter @alienplatform/ai-gateway build + + # Prebuild packages publish FIRST so an install of the wrapper can never + # race ahead of the optionalDependencies it pins. + - name: Publish prebuild packages (first) + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + VERSION: ${{ needs.prepare.outputs.version }} + run: | + for triple in darwin-arm64 darwin-x64 linux-x64-gnu linux-x64-musl linux-arm64-gnu linux-arm64-musl; do + package="@alienplatform/ai-gateway-${triple}" + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "Dry-running ${package}@${VERSION}..." + ( cd "packages/ai-gateway/npm/${triple}" && npm publish --access public --dry-run ) + continue + fi + + # npm has no multi-package transaction; this existence check makes + # a retry resume after a partial publish. + published="$(npm view "${package}@${VERSION}" version 2>/dev/null || true)" + if [ "$published" = "$VERSION" ]; then + echo "${package}@${VERSION} is already published; continuing" + else + echo "Publishing ${package}@${VERSION}..." + ( cd "packages/ai-gateway/npm/${triple}" && npm publish --access public ) + fi + 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 }} + VERSION: ${{ needs.prepare.outputs.version }} + run: | + if [ "${{ inputs.dry_run }}" = "true" ]; then + pnpm --filter @alienplatform/ai-gateway publish --access public --no-git-checks --dry-run + exit 0 + fi + + published="$(npm view "@alienplatform/ai-gateway@${VERSION}" version 2>/dev/null || true)" + if [ "$published" = "$VERSION" ]; then + echo "@alienplatform/ai-gateway@${VERSION} is already published; continuing" + else + pnpm --filter @alienplatform/ai-gateway publish --access public --no-git-checks + fi + # ─── Binary builds (4 targets, all parallel) ─────────────────────── build-binaries-linux-x86_64: diff --git a/.gitignore b/.gitignore index 787d92d06..bec988cde 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ rustc-ice-*.txt .codex/ .pnpm-store +packages/ai-gateway/npm/ diff --git a/Cargo.lock b/Cargo.lock index 823832b2a..3da3c7807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,9 +83,36 @@ dependencies = [ "memchr", ] +[[package]] +name = "alien-ai-gateway" +version = "2.1.6" +dependencies = [ + "alien-bindings", + "alien-core", + "alien-error", + "aws-config", + "aws-credential-types", + "aws-sigv4", + "aws-smithy-eventstream", + "aws-smithy-types", + "axum 0.8.9", + "base64 0.22.1", + "dotenvy", + "futures", + "http 1.4.2", + "httpmock", + "reqwest 0.12.28", + "serde", + "serde_json", + "temp-env", + "tokio", + "tracing", + "workspace_root", +] + [[package]] name = "alien-aws-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-client-core", @@ -127,7 +154,7 @@ dependencies = [ [[package]] name = "alien-azure-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-client-core", "alien-core", @@ -171,7 +198,7 @@ dependencies = [ [[package]] name = "alien-bindings" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -215,7 +242,7 @@ dependencies = [ [[package]] name = "alien-bindings-node" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-error", @@ -232,7 +259,7 @@ dependencies = [ [[package]] name = "alien-build" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-build", "alien-core", @@ -266,7 +293,7 @@ dependencies = [ [[package]] name = "alien-cli" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-build", @@ -337,7 +364,7 @@ dependencies = [ [[package]] name = "alien-cli-common" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-deployment", @@ -351,7 +378,7 @@ dependencies = [ [[package]] name = "alien-client-config" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -369,7 +396,7 @@ dependencies = [ [[package]] name = "alien-client-core" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "anyhow", @@ -388,7 +415,7 @@ dependencies = [ [[package]] name = "alien-cloudformation" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -403,7 +430,7 @@ dependencies = [ [[package]] name = "alien-commands" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -441,7 +468,7 @@ dependencies = [ [[package]] name = "alien-commands-client" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "base64 0.22.1", @@ -456,7 +483,7 @@ dependencies = [ [[package]] name = "alien-core" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "alien-macros", @@ -488,7 +515,7 @@ dependencies = [ [[package]] name = "alien-deploy-cli" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-cli-common", "alien-core", @@ -531,7 +558,7 @@ dependencies = [ [[package]] name = "alien-deployment" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -562,7 +589,7 @@ dependencies = [ [[package]] name = "alien-error" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error-derive", "anyhow", @@ -575,7 +602,7 @@ dependencies = [ [[package]] name = "alien-error-derive" -version = "2.0.2" +version = "2.1.6" dependencies = [ "proc-macro2", "quote", @@ -585,7 +612,7 @@ dependencies = [ [[package]] name = "alien-gcp-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-client-core", "alien-core", @@ -618,7 +645,7 @@ dependencies = [ [[package]] name = "alien-helm" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -632,7 +659,7 @@ dependencies = [ [[package]] name = "alien-infra" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -692,7 +719,7 @@ dependencies = [ [[package]] name = "alien-k8s-clients" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-client-core", "alien-core", @@ -721,7 +748,7 @@ dependencies = [ [[package]] name = "alien-local" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-build", @@ -764,7 +791,7 @@ dependencies = [ [[package]] name = "alien-macros" -version = "2.0.2" +version = "2.1.6" dependencies = [ "proc-macro2", "quote", @@ -773,7 +800,7 @@ dependencies = [ [[package]] name = "alien-manager" -version = "2.0.2" +version = "2.1.6" dependencies = [ "aegis", "alien-azure-clients", @@ -834,7 +861,7 @@ dependencies = [ [[package]] name = "alien-manager-api" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "chrono", @@ -853,7 +880,7 @@ dependencies = [ [[package]] name = "alien-observer" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -873,7 +900,7 @@ dependencies = [ [[package]] name = "alien-operator" -version = "2.0.2" +version = "2.1.6" dependencies = [ "aegis", "alien-client-config", @@ -917,7 +944,7 @@ dependencies = [ [[package]] name = "alien-permissions" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -936,7 +963,7 @@ dependencies = [ [[package]] name = "alien-platform-api" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "chrono", @@ -955,7 +982,7 @@ dependencies = [ [[package]] name = "alien-preflights" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -972,7 +999,7 @@ dependencies = [ [[package]] name = "alien-sdk" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-core", @@ -994,7 +1021,7 @@ dependencies = [ [[package]] name = "alien-terraform" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-core", "alien-error", @@ -1009,7 +1036,7 @@ dependencies = [ [[package]] name = "alien-test" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-aws-clients", "alien-azure-clients", @@ -1056,7 +1083,7 @@ dependencies = [ [[package]] name = "alien-test-app" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "alien-sdk", @@ -1102,7 +1129,7 @@ dependencies = [ [[package]] name = "alien-worker-protocol" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-error", "async-stream", @@ -1121,7 +1148,7 @@ dependencies = [ [[package]] name = "alien-worker-runtime" -version = "2.0.2" +version = "2.1.6" dependencies = [ "alien-bindings", "alien-commands", @@ -1831,6 +1858,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..445bb8177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "crates/alien-k8s-clients", "crates/alien-observer", "crates/alien-error", + "crates/alien-ai-gateway", "crates/alien-error-derive", "crates/alien-permissions", "crates/alien-preflights", @@ -48,6 +49,7 @@ resolver = "2" default-members = [ "crates/alien-core", "crates/alien-error", + "crates/alien-ai-gateway", "crates/alien-macros" ] @@ -64,6 +66,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-ai-gateway = { path = "crates/alien-ai-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 +101,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 +213,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/Cargo.toml b/crates/alien-ai-gateway/Cargo.toml new file mode 100644 index 000000000..693310e0b --- /dev/null +++ b/crates/alien-ai-gateway/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "alien-ai-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 } +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 } +dotenvy = { workspace = true } +workspace_root = { workspace = true } diff --git a/crates/alien-ai-gateway/src/availability.rs b/crates/alien-ai-gateway/src/availability.rs new file mode 100644 index 000000000..9b5f6e56f --- /dev/null +++ b/crates/alien-ai-gateway/src/availability.rs @@ -0,0 +1,229 @@ +//! Runtime model-availability filtering for `/v1/models`. +//! +//! The catalog (`ai_catalog`) is the static superset of what each cloud CAN serve. +//! What a specific deployment can ACTUALLY invoke depends on per-account/region +//! enablement (a Bedrock model-access grant, a Vertex Model Garden entitlement, an +//! Azure Foundry deployment) that no uniform cloud API reports. So we probe: a tiny +//! `max_tokens: 1` request per candidate model, signed with the workload's own +//! ambient credential, classified by status. This needs no permission beyond the +//! inference grant the workload already holds (`ai/invoke`). +//! +//! A 429 (rate-limit) means the endpoint authed and routed the request, so the model +//! is enabled and merely throttled. A 400 does NOT: Bedrock answers "The provided +//! model identifier is invalid" with 400 for a model the account cannot address, so +//! counting 400 as enabled lists models that then fail on the first real call. The +//! probe body is minimal and well formed, so a 400 is about the model, not the body. +//! 400/401/403/404 therefore all mean the model is off. Probing is lazy (first +//! `/v1/models` per binding) and cached, so it never +//! gates the gateway bind. Fail-open by design: `available_models` never returns +//! an error and never fails a deploy. A model that cannot be probed conclusively +//! stays listed (never worse than the old static catalog) and the result is left +//! uncached so the next call re-probes. + +use alien_core::{ + ai_catalog::{self, CatalogModel, Protocol}, + Platform, +}; +use alien_error::AlienError; +use serde_json::json; +use tracing::{debug, warn}; + +use crate::error::{ErrorData, Result}; +use crate::router::{ + bedrock_geo, missing_field, sign_and_execute, upstream_target, vertex_host, GatewayRoute, + FOUNDRY_ANTHROPIC_VERSION, +}; + +/// The outcome of probing one model. +enum Availability { + /// Reached, authed, and served (2xx, or a 429 rate-limit). + Available, + /// Definitively off: rejected the model or the caller (400/401/403/404). + Unavailable, + /// Could not tell (transport error, 5xx, or the route lacked a field to build + /// the probe). Kept in the list for this response, but the result is not cached. + Indeterminate, +} + +/// The filtered set plus whether every probe reached a definite verdict. When not +/// fully resolved, the caller must not cache the result (a transient error must not +/// stick a diminished list until redeploy). +pub(crate) struct ProbeResult { + pub models: Vec<&'static CatalogModel>, + pub fully_resolved: bool, +} + +/// Classify an upstream HTTP status. See the module doc for why 429 is "available" +/// and why 400 is not. +fn classify_status(code: u16) -> Availability { + match code { + 200..=299 | 429 => Availability::Available, + 400 | 401 | 403 | 404 => Availability::Unavailable, + _ => Availability::Indeterminate, + } +} + +/// Probe every catalog model for the route's cloud concurrently and keep the +/// enabled ones (plus any that could not be judged). Never errors. +pub(crate) async fn available_models( + route: &GatewayRoute, + client: &reqwest::Client, +) -> ProbeResult { + // Probe every candidate concurrently. join_all preserves input order, so the + // list stays in catalog order across calls. + let candidates = ai_catalog::models_for(route.cloud); + let probes: Vec<_> = candidates + .iter() + .copied() + .map(|cm| async move { (cm, probe_model(route, client, cm).await) }) + .collect(); + let verdicts = futures::future::join_all(probes).await; + + let mut models = Vec::new(); + let mut fully_resolved = true; + for (cm, verdict) in verdicts { + match verdict { + Availability::Available => models.push(cm), + Availability::Unavailable => { + debug!(model = cm.public_id, cloud = ?route.cloud, "model not enabled, dropping"); + } + Availability::Indeterminate => { + warn!(model = cm.public_id, cloud = ?route.cloud, "availability undetermined; keeping the model listed and leaving the result uncached"); + models.push(cm); + fully_resolved = false; + } + } + } + ProbeResult { models, fully_resolved } +} + +/// Send one `max_tokens: 1` request to the model's native endpoint and classify the +/// status. Any failure to build or send the probe is `Indeterminate`, never a panic. +async fn probe_model( + route: &GatewayRoute, + client: &reqwest::Client, + cm: &CatalogModel, +) -> Availability { + let built = match cm.protocol { + Protocol::OpenAi => openai_probe(route, cm), + Protocol::Anthropic => anthropic_probe(route, cm), + }; + let (url, service, body, extra_headers) = match built { + Ok(probe) => probe, + Err(error) => { + debug!(model = cm.public_id, %error, "could not build the availability probe"); + return Availability::Indeterminate; + } + }; + let header_refs: Vec<(&str, &str)> = + extra_headers.iter().map(|(k, v)| (*k, v.as_str())).collect(); + match sign_and_execute(client, &route.cred, &url, service, body, &header_refs).await { + Ok(resp) => classify_status(resp.status().as_u16()), + Err(error) => { + debug!(model = cm.public_id, %error, "availability probe did not reach the upstream"); + Availability::Indeterminate + } + } +} + +/// A minimal Chat Completions probe body: one user turn, one output token. +fn openai_body(upstream_id: &str) -> Vec { + json!({ + "model": upstream_id, + "max_tokens": 1, + "messages": [{ "role": "user", "content": "ping" }], + }) + .to_string() + .into_bytes() +} + +/// A minimal Anthropic Messages probe body for a given wire-version marker. +fn anthropic_body(version: &str) -> Vec { + json!({ + "anthropic_version": version, + "max_tokens": 1, + "messages": [{ "role": "user", "content": "ping" }], + }) + .to_string() + .into_bytes() +} + +type Probe = (String, &'static str, Vec, Vec<(&'static str, String)>); + +fn openai_probe(route: &GatewayRoute, cm: &CatalogModel) -> Result { + let (url, service) = upstream_target(route, Protocol::OpenAi)?; + Ok((url, service, openai_body(cm.upstream_id), Vec::new())) +} + +/// Build the same per-cloud Claude endpoint the proxy uses (Bedrock InvokeModel / +/// Vertex rawPredict / Foundry Anthropic), with a minimal body. +fn anthropic_probe(route: &GatewayRoute, cm: &CatalogModel) -> Result { + match route.cloud { + Platform::Aws => { + 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-runtime.{region}.amazonaws.com")); + let url = format!( + "{}/model/{}.{}/invoke", + base.trim_end_matches('/'), + bedrock_geo(region), + cm.upstream_id + ); + Ok((url, "bedrock", anthropic_body("bedrock-2023-05-31"), Vec::new())) + } + Platform::Gcp => { + 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 base = + route.upstream_base_override.clone().unwrap_or_else(|| vertex_host(location)); + let url = format!( + "{}/v1/projects/{project}/locations/{location}/publishers/anthropic/models/{}:rawPredict", + base.trim_end_matches('/'), + cm.upstream_id + ); + Ok((url, "", anthropic_body("vertex-2023-10-16"), Vec::new())) + } + Platform::Azure => { + let endpoint = + route.azure_endpoint.as_deref().ok_or_else(|| missing_field(route, "endpoint"))?; + let base = + route.upstream_base_override.clone().unwrap_or_else(|| endpoint.to_string()); + let url = format!("{}/anthropic/v1/messages", base.trim_end_matches('/')); + let body = json!({ + "model": cm.upstream_id, + "max_tokens": 1, + "messages": [{ "role": "user", "content": "ping" }], + }) + .to_string() + .into_bytes(); + Ok((url, "", body, vec![("anthropic-version", FOUNDRY_ANTHROPIC_VERSION.to_string())])) + } + cloud => Err(AlienError::new(ErrorData::Other { + message: format!("{cloud:?} does not serve the Anthropic protocol"), + })), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn classify_treats_429_as_available_and_400_as_off() { + // 2xx, plus the throttled-but-enabled case. + assert!(matches!(classify_status(200), Availability::Available)); + assert!(matches!(classify_status(429), Availability::Available)); + // 400 is the model, not the body: Bedrock returns it as "The provided model + // identifier is invalid", and listing those breaks the invocable contract. + assert!(matches!(classify_status(400), Availability::Unavailable)); + // Auth / entitlement / not-found: definitively off. + assert!(matches!(classify_status(401), Availability::Unavailable)); + assert!(matches!(classify_status(403), Availability::Unavailable)); + assert!(matches!(classify_status(404), Availability::Unavailable)); + // Anything else: can't tell. + assert!(matches!(classify_status(500), Availability::Indeterminate)); + assert!(matches!(classify_status(503), Availability::Indeterminate)); + } +} diff --git a/crates/alien-ai-gateway/src/bin/alien-ai-gateway.rs b/crates/alien-ai-gateway/src/bin/alien-ai-gateway.rs new file mode 100644 index 000000000..78526081a --- /dev/null +++ b/crates/alien-ai-gateway/src/bin/alien-ai-gateway.rs @@ -0,0 +1,116 @@ +//! 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 (ephemeral port, +//! or ALIEN_AI_GATEWAY_PORT when pinned) +//! 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_ai_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) + // Pin the child to this launcher's port so the URL is known out of band; + // the container's network namespace is isolated, so a fixed port is safe. + .env(PORT_ENV, port().to_string()) + .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_ai_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 { + // A pinned port (container bootstrap) is bound as-is; without one, bind an + // ephemeral port so the spawning parent (the SDK-spawn path) can read the + // real URL back from the line announced below. + let pinned = std::env::var(PORT_ENV).is_ok(); + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, if pinned { port() } else { 0 })); + let bindings = alien_ai_gateway::bindings_from_env() + .unwrap_or_else(|e| die(&format!("could not read the AI bindings: {e}"))); + let handle = alien_ai_gateway::start_gateway_on(bindings, addr) + .await + .unwrap_or_else(|e| die(&format!("gateway failed to start: {e}"))); + // Only the ephemeral (SDK-spawn) path needs the URL announced: the parent + // reads this one machine-readable line to learn the OS-assigned port. The + // container bootstrap already knows its fixed port, so stay silent there. The + // handshake relies on stdout carrying nothing but this line, so any future + // logging must go to stderr. Flush because the parent may stop reading stdout + // once it has the URL. + if !pinned { + println!("{}", serde_json::json!({ "aiGatewayUrl": &handle.url })); + let _ = std::io::Write::flush(&mut std::io::stdout()); + } + // Hold the process (and the server task) open for the lifetime of the parent. + 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-ai-gateway/src/config.rs b/crates/alien-ai-gateway/src/config.rs new file mode 100644 index 000000000..79baa0459 --- /dev/null +++ b/crates/alien-ai-gateway/src/config.rs @@ -0,0 +1,366 @@ +//! 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}; + +/// 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 { + match binding { + AiBinding::Bedrock(b) => Some(GatewayBinding { + name: name.to_string(), + cloud: Platform::Aws, + region: Some(b.region), + project: None, + azure_endpoint: None, + }), + AiBinding::Vertex(b) => Some(GatewayBinding { + name: name.to_string(), + cloud: Platform::Gcp, + region: Some(b.location), + project: Some(b.project), + azure_endpoint: None, + }), + AiBinding::Foundry(b) => Some(GatewayBinding { + name: name.to_string(), + cloud: Platform::Azure, + region: None, + project: None, + azure_endpoint: Some(b.endpoint), + }), + // 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, + }) +} + +#[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-ai-gateway/src/creds.rs b/crates/alien-ai-gateway/src/creds.rs new file mode 100644 index 000000000..d80f3085e --- /dev/null +++ b/crates/alien-ai-gateway/src/creds.rs @@ -0,0 +1,591 @@ +//! 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 chat/completions endpoint and +//! for Claude over classic InvokeModel, `bedrock-mantle` for the Responses API. +//! - 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 }, +} + +/// Bound the instance-metadata fetch. It feeds `/v1/models`, and single-flight holds the +/// cache lock across it, so without a timeout a hung metadata service would wedge every +/// waiting caller indefinitely. The endpoint is link-local and normally answers in well +/// under a second, so this only trips on a real hang. +const METADATA_TIMEOUT: Duration = Duration::from_secs(10); + +/// 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>, + /// Base URL for the instance metadata endpoint. `None` uses the real cloud host; + /// tests set it to point the metadata fetch at a mock server. + metadata_base: Option, +} + +impl BearerTokenCred { + pub fn gcp() -> Self { + Self { + source: BearerSource::Gcp, + client: reqwest::Client::new(), + cache: Mutex::new(None), + metadata_base: 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), + metadata_base: 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), + metadata_base: 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), + metadata_base: 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 { + // Hold the cache lock across the refresh so a burst of concurrent probes (the + // `/v1/models` fan-out authorizes every model at once) collapses to a single + // metadata fetch instead of stampeding the metadata service. `tokio::sync::Mutex` + // may be held across `.await`. + let mut cache = self.cache.lock().await; + if let Some((tok, exp)) = cache.as_ref() { + if Instant::now() < *exp { + return Ok(tok.clone()); + } + } + + let (url, header_name, header_value) = match source { + BearerSource::Gcp => ( + format!( + "{}/computeMetadata/v1/instance/service-accounts/default/token", + self.metadata_base.as_deref().unwrap_or("http://metadata.google.internal") + ), + "Metadata-Flavor", + "Google".to_string(), + ), + BearerSource::Azure { resource } => ( + format!( + "{}/metadata/identity/oauth2/token?api-version=2018-02-01&resource={resource}", + self.metadata_base.as_deref().unwrap_or("http://169.254.169.254") + ), + "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) + .timeout(METADATA_TIMEOUT) + .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)); + *cache = 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}" + ); + } + + #[tokio::test] + async fn concurrent_cold_cache_collapses_to_one_metadata_fetch() { + use httpmock::prelude::*; + + // A metadata server that counts how many token fetches actually reach it. + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(GET) + .path("/computeMetadata/v1/instance/service-accounts/default/token") + .header("Metadata-Flavor", "Google"); + then.status(200).json_body(serde_json::json!({ + "access_token": "tok-single-flight", + "expires_in": 3600, + "token_type": "Bearer", + })); + }) + .await; + + // A cold-cache GCP credential whose metadata fetch is aimed at the mock host. + let cred = Arc::new(BearerTokenCred { + source: BearerSource::Gcp, + client: reqwest::Client::new(), + cache: Mutex::new(None), + metadata_base: Some(server.base_url()), + }); + + // Fire many token fetches at once, exactly like the `/v1/models` probe fan-out that + // authorizes every catalog model concurrently. Single-flight must collapse them to + // one upstream fetch; without holding the lock across the refresh, each caller misses + // the cold cache and stampedes the metadata service. + let calls = (0..16).map(|_| { + let cred = cred.clone(); + tokio::spawn(async move { cred.token().await }) + }); + for joined in futures::future::join_all(calls).await { + let token = joined.expect("token task did not panic").expect("token fetch succeeds"); + assert_eq!(token, "tok-single-flight"); + } + + assert_eq!( + mock.hits_async().await, + 1, + "concurrent cold-cache fetches must collapse to a single metadata request" + ); + } +} diff --git a/crates/alien-ai-gateway/src/error.rs b/crates/alien-ai-gateway/src/error.rs new file mode 100644 index 000000000..12f1536d4 --- /dev/null +++ b/crates/alien-ai-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-ai-gateway/src/lib.rs b/crates/alien-ai-gateway/src/lib.rs new file mode 100644 index 000000000..1c5bbe60f --- /dev/null +++ b/crates/alien-ai-gateway/src/lib.rs @@ -0,0 +1,179 @@ +//! 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 availability; +mod config; +mod creds; +mod error; +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 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-ai-gateway/src/router/bedrock.rs b/crates/alien-ai-gateway/src/router/bedrock.rs new file mode 100644 index 000000000..64ef4364c --- /dev/null +++ b/crates/alien-ai-gateway/src/router/bedrock.rs @@ -0,0 +1,378 @@ +//! AWS Bedrock: serve Claude through classic `InvokeModel`, normalizing the Anthropic +//! Messages body to the pinned InvokeModel schema and decoding the event-stream reply. + +use alien_error::{AlienError, Context, IntoAlienError}; +use axum::body::{Body, Bytes}; +use axum::http::{header, HeaderMap, StatusCode}; +use axum::response::Response; +use futures::StreamExt; +use serde_json::{json, Map, Value}; +use tracing::warn; + +use super::eventstream::EventStreamToSse; +use super::{forward_response, missing_field, parse_stream_flag, sign_and_execute, GatewayRoute}; +use crate::error::{ErrorData, Result}; + +/// 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. +pub(crate) 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. +pub(crate) 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. +pub(crate) 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.) +pub(crate) fn bedrock_geo(region: &str) -> &'static str { + if region.starts_with("us-gov-") { + "us-gov" + } else if region.starts_with("us-") { + "us" + } else { + "global" + } +} diff --git a/crates/alien-ai-gateway/src/router/eventstream.rs b/crates/alien-ai-gateway/src/router/eventstream.rs new file mode 100644 index 000000000..0f9cb6585 --- /dev/null +++ b/crates/alien-ai-gateway/src/router/eventstream.rs @@ -0,0 +1,113 @@ +//! Response-side half of the AWS Bedrock arm: Claude's classic InvokeModel reply is +//! event-stream framed, so it decodes here, apart from the request path in `bedrock.rs`. + +use aws_smithy_eventstream::frame::{DecodedFrame, MessageFrameDecoder}; +use aws_smithy_types::event_stream::Message; +use base64::{engine::general_purpose::STANDARD, Engine}; +use serde_json::{json, Value}; + +/// 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)] +pub(crate) 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. + pub(crate) 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. + pub(crate) 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") +} diff --git a/crates/alien-ai-gateway/src/router/foundry.rs b/crates/alien-ai-gateway/src/router/foundry.rs new file mode 100644 index 000000000..40c5dc46b --- /dev/null +++ b/crates/alien-ai-gateway/src/router/foundry.rs @@ -0,0 +1,61 @@ +//! Azure Foundry: serve Claude through the `/anthropic/v1` endpoint — the native +//! Anthropic Messages API with the model in the body and a version header. + +use alien_error::{Context, IntoAlienError}; +use axum::http::HeaderMap; +use axum::response::Response; +use serde_json::Value; + +use super::bedrock::filtered_header_betas; +use super::{forward_response, missing_field, sign_and_execute, GatewayRoute}; +use crate::error::{ErrorData, Result}; + +/// The Messages API version the gateway bridges to Foundry's Anthropic endpoint; +/// Foundry reads it from the standard `anthropic-version` header. +pub(crate) 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. +pub(crate) 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) +} diff --git a/crates/alien-ai-gateway/src/router/mod.rs b/crates/alien-ai-gateway/src/router/mod.rs new file mode 100644 index 000000000..c7d6e1bb0 --- /dev/null +++ b/crates/alien-ai-gateway/src/router/mod.rs @@ -0,0 +1,1575 @@ +//! 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 axum::{ + body::{Body, Bytes}, + extract::{DefaultBodyLimit, Path, State}, + http::{header, HeaderMap, StatusCode}, + response::{IntoResponse, Json, Response}, + routing::{get, post}, + Router, +}; +use serde_json::{json, Value}; + +use crate::creds::AmbientCred; +use crate::error::{ErrorData, Result}; + +mod bedrock; +mod eventstream; +mod foundry; +mod vertex; + +use bedrock::proxy_bedrock_anthropic; +use foundry::proxy_foundry_anthropic; +use vertex::proxy_vertex_anthropic; + +// Re-exported so availability.rs and the test module below can resolve these per-provider +// items. `vertex_host` is also used by `upstream_target` here; `ensure_block_content` and +// `EventStreamToSse` are test-only. +pub(crate) use bedrock::bedrock_geo; +pub(crate) use foundry::FOUNDRY_ANTHROPIC_VERSION; +pub(crate) use vertex::vertex_host; +#[cfg(test)] +pub(crate) use bedrock::ensure_block_content; +#[cfg(test)] +pub(crate) use eventstream::EventStreamToSse; + +/// 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, +} + +struct AppState { + routes: HashMap, + client: reqwest::Client, + /// Per-binding availability cache: the enabled model subset, probed lazily on + /// the first `/v1/models` and reused for the process lifetime. Access grants + /// change only across a redeploy (which restarts us), so there is no TTL. + models_cache: HashMap>>>, +} + +/// 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: HashMap = + routes.into_iter().map(|r| (r.name.clone(), r)).collect(); + let models_cache = + routes.keys().map(|name| (name.clone(), tokio::sync::OnceCell::new())).collect(); + let state = Arc::new(AppState { + routes, + client: reqwest::Client::new(), + models_cache, + }); + 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)) + // This is a pure proxy, so the upstream enforces its own body size. Without + // this, axum's 2 MB default rejects legitimate large requests (base64 vision + // images, long tool-heavy conversations) with 413 before they ever leave us. + .layer(DefaultBodyLimit::disable()) + .with_state(state) +} + +/// 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. +pub(crate) 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 (mut payload, model) = parse_model_request(&body)?; + + // 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; + } + + payload["model"] = Value::String(cm.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 (url, aws_service) = upstream_target(route, cm.protocol)?; + + 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 models the binding's cloud actually has enabled, +/// in OpenAI list shape. The catalog is the superset; a per-cloud availability probe +/// (see `availability`) filters it to what this deployment can invoke, lazily on the +/// first call and cached thereafter. +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: binding.clone() }))?; + let cell = state + .models_cache + .get(&binding) + .ok_or_else(|| AlienError::new(ErrorData::UnknownBinding { binding: binding.clone() }))?; + + // Cache only a fully-resolved probe: if any model came back Indeterminate (a + // transient error), serve the current best but leave it uncached so the next + // call re-probes rather than sticking a diminished list until redeploy. + let models = match cell + .get_or_try_init(|| async { + let probed = crate::availability::available_models(route, &state.client).await; + let models = Arc::new(probed.models); + if probed.fully_resolved { + Ok(models) + } else { + Err(models) + } + }) + .await + { + Ok(cached) => Arc::clone(cached), + Err(fresh) => fresh, + }; + + let data: Vec = models + .iter() + .map(|m| { + json!({ + "id": m.public_id, + "object": "model", + "provider": m.provider(), + "displayName": m.display_name(), + }) + }) + .collect(); + Ok(Json(json!({ "object": "list", "data": data })).into_response()) +} + +/// The error for a binding missing a field a handler needs. +pub(crate) 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. +pub(crate) 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(), + })), + } +} + +#[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 aws_smithy_types::event_stream::Message; + use base64::{engine::general_purpose::STANDARD, Engine}; + 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()), + } + } + + 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, + } + } + + #[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, + } + } + + #[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; + } + + #[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 availability_filters_by_probe() { + // The gateway probes each catalog model and lists only the enabled ones. + // Mock upstream: the OpenAI chat endpoint answers 200 (those models are + // available), the Bedrock InvokeModel path answers 403 (Claude is not granted + // on this account), so /v1/models keeps the OpenAI models and drops Claude. + let server = MockServer::start_async().await; + let openai = server + .mock_async(|when, then| { + when.method(POST).path("/openai/v1/chat/completions"); + then.status(200).header("content-type", "application/json").body(r#"{"choices":[]}"#); + }) + .await; + let claude = server + .mock_async(|when, then| { + when.method(POST).matches(|req: &HttpMockRequest| req.path.contains("/model/")); + then.status(403).body("access denied"); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).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 data = body["data"].as_array().unwrap(); + let ids: Vec<&str> = data.iter().map(|m| m["id"].as_str().unwrap()).collect(); + + // An enabled OpenAI model is kept, enriched with provider + displayName. + assert!(ids.contains(&"gpt-oss-20b"), "gpt-oss-20b must be listed: {ids:?}"); + let gpt = data.iter().find(|m| m["id"] == "gpt-oss-20b").expect("gpt-oss-20b entry"); + assert_eq!(gpt["provider"], "openai"); + assert_eq!(gpt["displayName"], "GPT-OSS 20B"); + // Un-granted Claude (403) is filtered out. + assert!( + !ids.iter().any(|id| id.starts_with("claude")), + "Claude (403) must be filtered out: {ids:?}" + ); + assert!(claude.hits_async().await > 0, "the Claude InvokeModel path must be probed"); + + // A second call is served from cache: no new upstream probes. + let hits = openai.hits_async().await; + let _ = reqwest::get(format!("{url}/llm/v1/models")).await.unwrap(); + assert_eq!( + openai.hits_async().await, + hits, + "a cached /v1/models must not re-probe upstream" + ); + } + + #[tokio::test] + async fn indeterminate_probe_keeps_model_and_is_not_cached() { + // A 500 from the upstream cannot prove a model is off, so the model stays + // listed, and the result must not be cached: the next call re-probes so a + // transient outage never sticks a diminished list until redeploy. + let server = MockServer::start_async().await; + let openai = server + .mock_async(|when, then| { + when.method(POST).path("/openai/v1/chat/completions"); + then.status(500).body("upstream blew up"); + }) + .await; + let claude = server + .mock_async(|when, then| { + when.method(POST).matches(|req: &HttpMockRequest| req.path.contains("/model/")); + then.status(500).body("upstream blew up"); + }) + .await; + + let url = serve(build_router(vec![aws_route(&server.base_url())])).await; + + let resp = reqwest::get(format!("{url}/llm/v1/models")).await.unwrap(); + assert_eq!(resp.status(), 200); + let body: Value = resp.json().await.unwrap(); + let ids: Vec<&str> = + body["data"].as_array().unwrap().iter().map(|m| m["id"].as_str().unwrap()).collect(); + assert!( + ids.contains(&"gpt-oss-20b") && ids.iter().any(|id| id.starts_with("claude")), + "an indeterminate probe must keep the model listed: {ids:?}" + ); + + // Not cached: the second call probes again. + let hits = openai.hits_async().await + claude.hits_async().await; + let _ = reqwest::get(format!("{url}/llm/v1/models")).await.unwrap(); + assert!( + openai.hits_async().await + claude.hits_async().await > hits, + "an indeterminate result must be re-probed on the next call, not cached" + ); + } +} diff --git a/crates/alien-ai-gateway/src/router/vertex.rs b/crates/alien-ai-gateway/src/router/vertex.rs new file mode 100644 index 000000000..b0fc70af5 --- /dev/null +++ b/crates/alien-ai-gateway/src/router/vertex.rs @@ -0,0 +1,74 @@ +//! GCP Vertex AI: serve Claude through `rawPredict` / `streamRawPredict`, the native +//! Anthropic Messages API with the model id in the URL and Vertex's version marker. + +use alien_error::{AlienError, Context, IntoAlienError}; +use axum::http::HeaderMap; +use axum::response::Response; +use serde_json::{json, Value}; + +use super::bedrock::filtered_header_betas; +use super::{forward_response, missing_field, parse_stream_flag, sign_and_execute, GatewayRoute}; +use crate::error::{ErrorData, Result}; + +/// 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. +pub(crate) 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. +pub(crate) 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) +} diff --git a/crates/alien-ai-gateway/tests/integration.rs b/crates/alien-ai-gateway/tests/integration.rs new file mode 100644 index 000000000..318b07739 --- /dev/null +++ b/crates/alien-ai-gateway/tests/integration.rs @@ -0,0 +1,208 @@ +//! 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_ai_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; + // /v1/models probes every catalog model for availability; answer the Claude + // InvokeModel path so Claude stays listed for the catalog assertions below. + // Probes this mock server matches nowhere (the other OpenAI-protocol models) + // return 404 and drop from the list, which the assertions tolerate. + let _aws_claude_probe = aws_upstream + .mock_async(|when, then| { + when.method(POST).matches(|req: &HttpMockRequest| req.path.contains("/model/")); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"msg_probe","content":[{"type":"text","text":"ok"}]}"#); + }) + .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()), + }, + 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()), + }, + ]; + + 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"); +} + +#[tokio::test] +async fn large_body_reaches_the_upstream_instead_of_413() { + // A permissive upstream that accepts the chat/completions path regardless of size. + let upstream = MockServer::start_async().await; + let upstream_mock = upstream + .mock_async(|when, then| { + when.method(POST).path("/openai/v1/chat/completions"); + then.status(200) + .header("content-type", "application/json") + .body(r#"{"id":"ok","choices":[{"message":{"content":"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(upstream.base_url()), + }]; + let base = serve(build_router(routes)).await; + let client = reqwest::Client::new(); + + // A body larger than axum's 2 MB default limit — a single base64 vision image is + // already this big. Without DefaultBodyLimit::disable() axum answers 413 before the + // proxy runs; with it, the body reaches the upstream and comes back 200. + let big_prompt = "x".repeat(3 * 1024 * 1024); + let resp = client + .post(format!("{base}/llm/v1/chat/completions")) + .json(&json!({"model":"gpt-oss-20b","messages":[{"role":"user","content":big_prompt}]})) + .send() + .await + .expect("large request"); + + assert_eq!(resp.status(), 200, "a >2 MB body must reach the upstream, not be rejected as 413"); + assert!(resp.text().await.unwrap().contains("pong")); + upstream_mock.assert_async().await; +} diff --git a/crates/alien-ai-gateway/tests/launcher.rs b/crates/alien-ai-gateway/tests/launcher.rs new file mode 100644 index 000000000..f71e1de13 --- /dev/null +++ b/crates/alien-ai-gateway/tests/launcher.rs @@ -0,0 +1,97 @@ +//! 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::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; + +// 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"); +} + +// The SDK-spawn path: `--gateway-serve` with no pinned port must bind an ephemeral +// port and print exactly one machine-readable URL line to stdout, then serve a +// reachable gateway at that URL. This is the contract the TS side (gateway.ts) +// parses; it is the only test that drives the real binary end to end. +#[test] +fn gateway_serve_announces_a_reachable_ephemeral_url() { + let exe = env!("CARGO_BIN_EXE_alien-ai-gateway"); + // env_clear so no stray ALIEN_AI_GATEWAY_PORT forces a fixed port and no + // ALIEN_*_BINDING is present (empty bindings still serve /healthz). + let mut child = Command::new(exe) + .env_clear() + .arg("--gateway-serve") + .stdout(Stdio::piped()) + .spawn() + .expect("gateway-serve should start"); + + let mut line = String::new(); + let read = BufReader::new(child.stdout.take().expect("piped stdout")).read_line(&mut line); + let url = read + .ok() + .and_then(|_| serde_json::from_str::(line.trim()).ok()) + .and_then(|v| v["aiGatewayUrl"].as_str().map(str::to_owned)); + // Probe reachability while the child is still alive. + let reachable = url.as_deref().map(alien_ai_gateway::wait_until_ready_blocking); + + // Reap unconditionally, before any assertion, so a broken binary (crash before + // printing, malformed output, missing field) can't leave the `--gateway-serve` + // child orphaned on its `pending` future. + let _ = child.kill(); + let _ = child.wait(); + + let url = url.expect("gateway-serve must print a JSON line carrying aiGatewayUrl"); + assert!(url.starts_with("http://127.0.0.1:"), "expected a loopback URL, got {url:?}"); + assert!(!url.ends_with(":0"), "must report the OS-assigned port, not :0: {url:?}"); + assert_eq!(reachable, Some(true), "the announced gateway URL must be reachable: {url}"); +} + +// 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-ai-gateway/tests/live_bedrock.rs b/crates/alien-ai-gateway/tests/live_bedrock.rs new file mode 100644 index 000000000..51f8f6e9d --- /dev/null +++ b/crates/alien-ai-gateway/tests/live_bedrock.rs @@ -0,0 +1,171 @@ +//! Live verification against real AWS Bedrock. Reads the shared alien-test-target +//! credentials from the workspace-root `.env.test`, so it runs in the credentialed +//! e2e job (and locally whenever that file is present). +//! +//! 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, with no static key and no mock. + +use std::net::Ipv4Addr; + +use alien_core::Platform; +use alien_ai_gateway::{build_router, AmbientCred, AwsSigV4Cred, GatewayRoute}; +use serde_json::{json, Value}; + +/// Load the shared alien-test-target credentials from the workspace-root +/// `.env.test` and expose the AWS target account under the SDK default-chain +/// variable names, so the SigV4 signer resolves them exactly as a deployed +/// workload's ambient identity would. +fn load_test_env() { + let root = workspace_root::get_workspace_root(); + dotenvy::from_path(root.join(".env.test")).expect("load .env.test from the workspace root"); + for (from, to) in [ + ("AWS_TARGET_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"), + ("AWS_TARGET_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY"), + ] { + // Fail loud rather than fall through to whatever AWS creds are already + // ambient (e.g. a developer's own profile): this test must sign as the + // alien-test-target account, so a missing var is a setup error. + let value = std::env::var(from).unwrap_or_else(|_| panic!("{from} must be set in .env.test")); + std::env::set_var(to, 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] +async fn live_bedrock_openai_chat() { + load_test_env(); + 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, + }; + + 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}"); + + // A 429 rate-limit is exactly what this test proves: Bedrock accepted the Rust + // SigV4 signature and routed the request (it reached the per-account daily token + // quota, well past auth). A signing regression would surface as 401/403 instead. + assert!( + status.is_success() || status == reqwest::StatusCode::TOO_MANY_REQUESTS, + "Bedrock must accept the Rust-signed request (2xx, or 429 when the daily token quota is spent); got {status}: {body}" + ); + // Only assert on the completion text when a completion actually came back. + if status.is_success() { + 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 = "Bedrock Claude via classic InvokeModel; reads .env.test but needs a Bedrock Claude model grant on alien-test-target (pending); un-ignore once granted"] +async fn live_bedrock_claude_streaming() { + load_test_env(); + // 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, + }; + + 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-ai-gateway/tests/live_foundry_claude.rs b/crates/alien-ai-gateway/tests/live_foundry_claude.rs new file mode 100644 index 000000000..11bec6f11 --- /dev/null +++ b/crates/alien-ai-gateway/tests/live_foundry_claude.rs @@ -0,0 +1,107 @@ +//! Live verification of Claude on Azure Foundry. Ignored by default because it +//! makes a real inference call and needs a Foundry endpoint plus a short-lived +//! Entra token in the environment. Verified green against the alien-test-target +//! resource alien-ai-foundry-e2e1 (RG alien-ai-e2e). +//! +//! 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-ai-gateway --test live_foundry_claude -- --ignored --nocapture +//! +//! Host and audience are settled: production bindings carry the AIServices +//! account's `properties.endpoint` (the `cognitiveservices.azure.com` shape), and +//! Foundry accepts that host with an `ai.azure.com`-audience token on the first +//! probe, with no host derivation or audience swap needed. + +use std::net::Ipv4Addr; + +use alien_core::Platform; +use alien_ai_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, + } +} + +#[tokio::test] +#[ignore = "hits real Foundry Claude; verified green against alien-ai-foundry-e2e1. Needs a minted (short-lived) AZURE_ACCESS_TOKEN, so it can't run in static-cred CI; see the module docstring to run it"] +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; verified green against alien-ai-foundry-e2e1. Needs a minted (short-lived) AZURE_ACCESS_TOKEN, so it can't run in static-cred CI; see the module docstring to run it"] +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-ai-gateway/tests/live_vertex_claude.rs b/crates/alien-ai-gateway/tests/live_vertex_claude.rs new file mode 100644 index 000000000..75577269e --- /dev/null +++ b/crates/alien-ai-gateway/tests/live_vertex_claude.rs @@ -0,0 +1,104 @@ +//! 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-ai-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_ai_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, + } +} + +#[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-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..b252da790 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; diff --git a/crates/alien-azure-clients/src/lib.rs b/crates/alien-azure-clients/src/lib.rs index 21e266578..8b6fc5373 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}; 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..2673dc183 100644 --- a/crates/alien-build/src/toolchain/native_addon.rs +++ b/crates/alien-build/src/toolchain/native_addon.rs @@ -28,18 +28,72 @@ 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"; +/// The native-asset shape a package embeds into a compiled binary. +enum AddonKind { + /// A napi-rs addon: `..node`, sourced from a per-triple + /// napi prebuild, a workspace dev build, or the napi CLI. + Napi { crate_dir: &'static str }, + /// A standalone launcher executable, sourced from a per-triple binary + /// prebuild, the workspace cargo target dir, or a host `cargo build`. + Binary { + /// The executable's file name (also its source name in a prebuild). + bin_name: &'static str, + /// The workspace crate producing the `[[bin]]` (`cargo build -p …`). + cargo_package: &'static str, + }, +} + +/// One package whose native asset a compiled binary embeds statically. Two ship +/// today: `@alienplatform/bindings` (a napi addon: kv/storage/queue/vault/ +/// container) and `@alienplatform/ai-gateway` (a launcher binary the `ai()` +/// client spawns). Each stages its own asset 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 x from "./"` next to `dist/native.js`). + staged_file: &'static str, + /// Whether the asset is a napi addon or a standalone binary; this drives + /// where the source is found and how it is (re)built. + kind: AddonKind, +} -/// 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", + kind: AddonKind::Napi { + crate_dir: "alien-bindings-node", + }, +}; + +/// The AI-gateway launcher binary: staged best-effort. A Worker resolves the +/// ai-gateway package transitively through the SDK even when it never calls +/// `ai()`, so a missing binary for the target is skipped (not a build error): +/// requiring it would regress every non-AI Worker. When present it is embedded +/// so a compiled `ai()`/`getAiConnection()` can extract and spawn it. +const AI_GATEWAY: NativeAddonSpec = NativeAddonSpec { + package: "@alienplatform/ai-gateway", + scoped_name: "ai-gateway", + staged_file: "alien-ai-gateway.bin", + kind: AddonKind::Binary { + bin_name: "alien-ai-gateway", + cargo_package: "alien-ai-gateway", + }, +}; /// Map a build target to the napi triple used in prebuild package names /// (`@alienplatform/bindings-`) and addon file names /// (`alien-bindings-node..node`). Mirrors `platformTriple()` in /// `packages/bindings/src/loader.ts`. `None` means no addon exists for the -/// target. +/// target. `Binary` assets (the ai-gateway launcher) key their per-triple +/// prebuild packages off this same triple. fn napi_triple(target: BinaryTarget) -> Option<&'static str> { match target { BinaryTarget::LinuxX64 => Some("linux-x64-gnu"), @@ -69,21 +123,23 @@ fn napi_triple(target: BinaryTarget) -> Option<&'static str> { /// /// Returns `Ok(None)` when no source exists (the caller turns that into a /// build error naming the missing prebuild package). -async fn find_addon_source( +async fn find_napi_source( src_dir: &Path, - bindings_dist: &Path, + spec: &NativeAddonSpec, + addon_dist: &Path, triple: &str, + crate_name: &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 +150,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,15 +164,15 @@ 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"); - if crate_dir.is_dir() { - workspace_addon_crate = Some(crate_dir.clone()); - let dev_addon = crate_dir.join(addon_file_name); + let crate_path = current.join("crates").join(crate_name); + if crate_path.is_dir() { + workspace_addon_crate = Some(crate_path.clone()); + let dev_addon = crate_path.join(addon_file_name); if dev_addon.is_file() { return Ok(Some(dev_addon)); } @@ -126,7 +182,8 @@ async fn find_addon_source( dir = current.parent(); } checked.push(format!( - "(no crates/alien-bindings-node above {})", + "(no crates/{} above {})", + crate_name, anchor.display() )); } @@ -184,56 +241,179 @@ 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. +/// Locate the launcher binary for `triple`, trying (in order): +/// +/// 1. The per-platform prebuild package (`@alienplatform/ai-gateway-`, +/// an `optionalDependency` that ships the executable), as a sibling of the +/// resolved package and in the app's own `node_modules`. +/// 2. The workspace cargo target dir (`target/release` then `target/debug`), +/// found by walking up to the crate that produces the bin. Only usable when +/// building for the host arch, since the dev binary is host-native. +/// 3. In-repo host-triple build: `cargo build --release --bin -p `. +/// +/// Returns `Ok(None)` when no source exists (the caller skips this best-effort +/// asset). +async fn find_binary_source( + src_dir: &Path, + spec: &NativeAddonSpec, + addon_dist: &Path, + triple: &str, + bin_name: &str, + cargo_package: &str, + resource_name: &str, + checked: &mut Vec, +) -> Result> { + // 1a. Prebuild linked next to the resolved package. + if let Some(scope_dir) = addon_dist.parent().and_then(Path::parent) { + let sibling = scope_dir + .join(format!("{}-{}", spec.scoped_name, triple)) + .join(bin_name); + if sibling.is_file() { + return Ok(Some(sibling)); + } + checked.push(sibling.display().to_string()); + } + // 1b. Prebuild installed in the app's own node_modules. + let prebuild = src_dir + .join("node_modules") + .join(format!("{}-{}", spec.package, triple)) + .join(bin_name); + if prebuild.is_file() { + return Ok(Some(prebuild)); + } + checked.push(prebuild.display().to_string()); + + // 2. Workspace cargo target. Walk up (from the app and the resolved dist) to + // the crate that produces the bin; its workspace root holds `target/`. + let canonical_dist = addon_dist.canonicalize().ok(); + let mut workspace_root: 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 { + if current.join("crates").join(cargo_package).is_dir() { + workspace_root = Some(current.to_path_buf()); + break 'anchors; + } + dir = current.parent(); + } + } + let Some(root) = workspace_root else { + checked.push(format!( + "(no crates/{cargo_package} above the app or resolved package)" + )); + return Ok(None); + }; + + // The dev/target binary is host-native, so only usable when building for the + // host triple; a cross target must come from a prebuild. + let host_triple = napi_triple(BinaryTarget::current_os()); + if host_triple != Some(triple) { + checked.push(format!( + "(cargo build skipped: host triple {host_triple:?} != target '{triple}')" + )); + return Ok(None); + } + for profile in ["release", "debug"] { + let candidate = root.join("target").join(profile).join(bin_name); + if candidate.is_file() { + return Ok(Some(candidate)); + } + checked.push(candidate.display().to_string()); + } + + // 3. Host build. + info!( + "Gateway binary {} not built yet; running `cargo build --release --bin {} -p {}` in {}", + bin_name, + bin_name, + cargo_package, + root.display() + ); + let output = Command::new("cargo") + .args(["build", "--release", "--bin", bin_name, "-p", cargo_package]) + .current_dir(&root) + .output() + .await + .into_alien_error() + .context(ErrorData::ImageBuildFailed { + resource_name: resource_name.to_string(), + reason: format!("Failed to execute `cargo build --bin {bin_name} -p {cargo_package}`"), + build_output: None, + })?; + if !output.status.success() { + let mut build_output = String::from_utf8_lossy(&output.stdout).into_owned(); + build_output.push_str(&String::from_utf8_lossy(&output.stderr)); + return Err(AlienError::new(ErrorData::ImageBuildFailed { + resource_name: resource_name.to_string(), + reason: format!("`cargo build --release --bin {bin_name} -p {cargo_package}` failed"), + build_output: Some(build_output), + })); + } + let built = root.join("target").join("release").join(bin_name); + if built.is_file() { + Ok(Some(built)) + } else { + Ok(None) + } +} + +/// 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 +421,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 +431,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 +440,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 +454,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 +468,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,34 +498,66 @@ 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 -/// up from `anchor` (typically the realpath of the resolved bindings -/// 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; +/// Walk up from `anchor` to the first `/crates/` that exists. +fn find_workspace_crate(anchor: &Path, name: &str) -> Option { let mut dir = Some(anchor); while let Some(current) = dir { - let candidate = current.join("crates").join("alien-bindings-node"); + let candidate = current.join("crates").join(name); if candidate.is_dir() { - crate_dir = Some(candidate); - break; + return Some(candidate); } dir = current.parent(); } - let Some(crate_dir) = crate_dir else { - return Vec::new(); - }; + None +} - 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(); +/// Workspace-built native assets that exist for the requested targets, walking +/// up from `anchor` (typically the realpath of the resolved package). Used by +/// the build cache key: the compiled binary embeds these bytes, so a rebuilt +/// addon or launcher binary must invalidate cached artifacts. +pub(crate) fn workspace_addon_inputs(anchor: &Path, targets: &[BinaryTarget]) -> Vec { + let mut inputs: Vec = Vec::new(); + for spec in [&BINDINGS, &AI_GATEWAY] { + match &spec.kind { + AddonKind::Napi { crate_dir } => { + let Some(crate_path) = find_workspace_crate(anchor, crate_dir) else { + continue; + }; + inputs.extend( + targets + .iter() + .filter_map(|target| napi_triple(*target)) + .map(|triple| crate_path.join(format!("{crate_dir}.{triple}.node"))) + .filter(|path| path.is_file()), + ); + } + AddonKind::Binary { + bin_name, + cargo_package, + } => { + // The host-built launcher binary under the workspace target dir. + let Some(root) = + find_workspace_crate(anchor, cargo_package).and_then(|c| { + c.parent().and_then(Path::parent).map(Path::to_path_buf) + }) + else { + continue; + }; + inputs.extend( + ["release", "debug"] + .iter() + .map(|profile| root.join("target").join(profile).join(bin_name)) + .filter(|path| path.is_file()), + ); + } + } + } inputs.sort(); inputs.dedup(); inputs @@ -366,108 +580,197 @@ 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. +/// The build error when a required native asset has no source for `target`, +/// tailored to its kind (napi addon vs launcher binary). +fn missing_source_reason( + spec: &NativeAddonSpec, + triple: &str, + target: BinaryTarget, + checked: &[String], +) -> String { + let pkg = spec.package; + let checked = checked.join(", "); + match &spec.kind { + AddonKind::Napi { crate_dir } => { + let lib_name = format!("lib{}.so", crate_dir.replace('-', "_")); + format!( + "{pkg} is installed, but the native addon for target '{target}' was not found. \ + Install the prebuild package '{pkg}-{triple}' (it ships {crate_dir}.{triple}.node), \ + or, in the alien workspace, build the dev addon with \ + `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/{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/{lib_name} {crate_dir}.{triple}.node'`. \ + Checked: {checked}." + ) + } + AddonKind::Binary { + bin_name, + cargo_package, + } => format!( + "{pkg} is installed, but the launcher binary for target '{target}' was not found. \ + Install the prebuild package '{pkg}-{triple}' (it ships {bin_name}), or, in the \ + alien workspace building for the host, build it with \ + `cargo build --release --bin {bin_name} -p {cargo_package}`. Checked: {checked}." + ), + } +} + +/// 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 mut checked = Vec::new(); - let Some(source) = find_addon_source( - src_dir, - bindings_dist, - triple, - &addon_file_name, - resource_name, - &mut checked, - ) - .await? - else { + let source = match &spec.kind { + AddonKind::Napi { crate_dir } => { + let addon_file_name = format!("{crate_dir}.{triple}.node"); + find_napi_source( + src_dir, + spec, + addon_dist, + triple, + crate_dir, + &addon_file_name, + resource_name, + &mut checked, + ) + .await? + } + AddonKind::Binary { + bin_name, + cargo_package, + } => { + find_binary_source( + src_dir, + spec, + addon_dist, + triple, + bin_name, + cargo_package, + resource_name, + &mut checked, + ) + .await? + } + }; + let Some(source) = source else { + if !required { + info!( + "Optional native asset {} for target '{}' not found; skipping (the app may not \ + use it). Checked: {}", + spec.package, + target, + checked.join(", ") + ); + return Ok(None); + } 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. \ - 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 \ - 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}'`. \ - Checked: {checked}.", - pkg = BINDINGS_PACKAGE, - checked = checked.join(", "), - ), + reason: missing_source_reason(spec, triple, target, &checked), build_output: None, })); }; // 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 +792,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 +814,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 +864,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 +911,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 +940,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 +968,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 +983,81 @@ 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 launcher binary IS present (shipped by the per-triple + /// prebuild package as the bare `alien-ai-gateway` executable) it stages under + /// its own literal file name (`alien-ai-gateway.bin`) next to its dist/native.js. + #[tokio::test] + async fn optional_ai_gateway_binary_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 binary_bytes = b"fake-ai-gateway-linux-arm64-binary"; + fs::write(prebuild_dir.join("alien-ai-gateway"), binary_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 binary must stage"); + assert_eq!(staged, ai_dist.join(AI_GATEWAY.staged_file)); + assert_eq!(fs::read(&staged).await.unwrap(), binary_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..a9c33a2f6 --- /dev/null +++ b/crates/alien-cloudformation/src/emitters/aws/ai.rs @@ -0,0 +1,145 @@ +//! 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 `ai/invoke` 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, + uniquify_iam_statement_sids, + }, + service_account::permission_context, + }, + template::{CfExpression, CfResource}, +}; +use alien_core::{import::EmitContext, Ai, ErrorData, PermissionProfile, PermissionSetReference, Result}; +use alien_error::{AlienError, Context, IntoAlienError}; +use alien_permissions::{generators::AwsCloudFormationPermissionsGenerator, BindingTarget}; + +#[derive(Debug, Clone, Copy, Default)] +pub struct AwsAiEmitter; + +impl CfEmitter for AwsAiEmitter { + fn emit_resources(&self, ctx: &EmitContext<'_>) -> Result> { + resource_config::(ctx, Ai::RESOURCE_TYPE)?; + ai_iam_policies(ctx) + } + + 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 +} + 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 7b97a68ae..4ae5e93a5 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..653853941 --- /dev/null +++ b/crates/alien-cloudformation/tests/generator/aws_ai_tests.rs @@ -0,0 +1,77 @@ +//! 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}; + +#[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}" + ); +} 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..491a6c4b6 --- /dev/null +++ b/crates/alien-core/src/ai_catalog.rs @@ -0,0 +1,532 @@ +//! 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, +} + +/// The one-time action, if any, a customer must take in the cloud provider before +/// the gateway can invoke a model. Static per (provider, cloud), surfaced in docs +/// and the example README. Distinct from runtime availability, which +/// `getAvailableModels` probes live per deployment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Activation { + /// Enabled by default; nothing for the customer to do (quota still applies). + OutOfBox, + /// Needs a one-time customer action first; the string says what. + RequiresOneTimeStep(&'static str), +} + +/// 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, +} + +impl CatalogModel { + /// The model's publisher, for grouping in a picker. Derived from the public id, + /// so the same public id reports the same provider on every cloud. + pub fn provider(&self) -> &'static str { + let id = self.public_id; + if id.starts_with("claude") { + "anthropic" + } else if id.starts_with("gpt") || id == "model-router" { + "openai" + } else if id.starts_with("gemini") || id.starts_with("gemma") { + "google" + } else if id.starts_with("qwen") { + "qwen" + } else if id.starts_with("deepseek") { + "deepseek" + } else if id.starts_with("mistral") + || id.starts_with("devstral") + || id.starts_with("magistral") + || id.starts_with("ministral") + { + "mistral" + } else if id.starts_with("minimax") { + "minimax" + } else if id.starts_with("kimi") { + "moonshotai" + } else if id.starts_with("nemotron") { + "nvidia" + } else if id.starts_with("glm") { + "zai" + } else if id.starts_with("palmyra") { + "writer" + } else { + "unknown" + } + } + + /// A human label for a model picker. Curated per id rather than derived so the + /// acronyms (GPT, OSS, GLM, VL) and versions read correctly. + pub fn display_name(&self) -> &'static str { + match self.public_id { + "gpt-oss-20b" => "GPT-OSS 20B", + "gpt-oss-120b" => "GPT-OSS 120B", + "gpt-oss-safeguard-20b" => "GPT-OSS Safeguard 20B", + "gpt-oss-safeguard-120b" => "GPT-OSS Safeguard 120B", + "deepseek-v3.2" => "DeepSeek V3.2", + "qwen3-32b" => "Qwen3 32B", + "qwen3-coder-30b" => "Qwen3 Coder 30B", + "qwen3-coder-next" => "Qwen3 Coder Next", + "qwen3-next-80b" => "Qwen3 Next 80B", + "qwen3-vl-235b" => "Qwen3 VL 235B", + "mistral-large-3" => "Mistral Large 3", + "devstral-2" => "Devstral 2", + "magistral-small" => "Magistral Small", + "ministral-3-14b" => "Ministral 3 14B", + "ministral-3-8b" => "Ministral 3 8B", + "ministral-3-3b" => "Ministral 3 3B", + "minimax-m2" => "MiniMax M2", + "minimax-m2.1" => "MiniMax M2.1", + "minimax-m2.5" => "MiniMax M2.5", + "kimi-k2.5" => "Kimi K2.5", + "nemotron-nano-9b" => "Nemotron Nano 9B", + "nemotron-nano-12b" => "Nemotron Nano 12B", + "nemotron-nano-3-30b" => "Nemotron Nano 3 30B", + "nemotron-super-3-120b" => "Nemotron Super 3 120B", + "gemma-3-4b" => "Gemma 3 4B", + "gemma-3-12b" => "Gemma 3 12B", + "gemma-3-27b" => "Gemma 3 27B", + "glm-4.7" => "GLM 4.7", + "glm-4.7-flash" => "GLM 4.7 Flash", + "glm-5" => "GLM 5", + "palmyra-vision-7b" => "Palmyra Vision 7B", + "claude-sonnet-5" => "Claude Sonnet 5", + "claude-opus-4.8" => "Claude Opus 4.8", + "claude-opus-4.7" => "Claude Opus 4.7", + "claude-opus-4.6" => "Claude Opus 4.6", + "claude-opus-4.5" => "Claude Opus 4.5", + "claude-opus-4.1" => "Claude Opus 4.1", + "claude-sonnet-4.6" => "Claude Sonnet 4.6", + "claude-sonnet-4.5" => "Claude Sonnet 4.5", + "claude-haiku-4.5" => "Claude Haiku 4.5", + "claude-fable-5" => "Claude Fable 5", + "claude-mythos-5" => "Claude Mythos 5", + "gemini-2.5-pro" => "Gemini 2.5 Pro", + "gemini-2.5-flash" => "Gemini 2.5 Flash", + "gemini-2.5-flash-lite" => "Gemini 2.5 Flash Lite", + "gemini-3.5-flash" => "Gemini 3.5 Flash", + "gemini-3.1-flash-lite" => "Gemini 3.1 Flash Lite", + "gpt-4.1" => "GPT-4.1", + "gpt-4o-mini" => "GPT-4o mini", + "model-router" => "Model Router", + other => other, + } + } + + /// The one-time enablement step for this model on its cloud, if any. Only Claude + /// needs one today, and the step differs per cloud. + pub fn activation(&self) -> Activation { + if !self.public_id.starts_with("claude") { + return Activation::OutOfBox; + } + match self.cloud { + Platform::Aws => Activation::RequiresOneTimeStep( + "Submit the one-time Anthropic use-case form in the Bedrock console.", + ), + Platform::Gcp => Activation::RequiresOneTimeStep( + "Enable Claude in Vertex AI Model Garden and accept Anthropic's terms of service, one-time, in the Google Cloud console.", + ), + Platform::Azure => Activation::RequiresOneTimeStep( + "Accept the Marketplace terms and create the Claude deployment in the Microsoft Foundry portal (one-time).", + ), + _ => Activation::OutOfBox, + } + } +} + +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. These stay + // in the catalog as the deployment-name contract, but the gateway's /v1/models + // availability probe drops any that the portal step has not created, so the + // list omits any Claude that Foundry would 404. + 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\""); + } + + #[test] + fn every_model_has_provider_display_name_and_activation() { + for m in CATALOG { + assert_ne!(m.provider(), "unknown", "no provider mapping for '{}'", m.public_id); + assert_ne!( + m.display_name(), + m.public_id, + "no curated display_name for '{}'", + m.public_id + ); + // Only Claude needs a one-time step; everything else is out of the box. + let is_claude = m.public_id.starts_with("claude"); + match m.activation() { + Activation::OutOfBox => { + assert!(!is_claude, "'{}' (Claude) must require a one-time step", m.public_id) + } + Activation::RequiresOneTimeStep(summary) => { + assert!(is_claude, "'{}' must be out of the box", m.public_id); + assert!(!summary.is_empty(), "'{}' step summary is empty", m.public_id); + } + } + } + } +} diff --git a/crates/alien-core/src/bin/schema_exporter.rs b/crates/alien-core/src/bin/schema_exporter.rs index 36e3fe441..67c45eccf 100644 --- a/crates/alien-core/src/bin/schema_exporter.rs +++ b/crates/alien-core/src/bin/schema_exporter.rs @@ -91,6 +91,8 @@ use utoipa::OpenApi; AwsOpenSearchOutputs, Postgres, PostgresOutputs, + Ai, + AiOutputs, 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..f2552cf1d --- /dev/null +++ b/crates/alien-core/src/bindings/ai.rs @@ -0,0 +1,193 @@ +//! 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), +} + +/// 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, +} + +/// 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, +} + +/// 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, +} + +/// 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(), + }) + } + + pub fn vertex(project: impl Into, location: impl Into) -> Self { + Self::Vertex(VertexAiBinding { + project: project.into(), + location: location.into(), + }) + } + + pub fn foundry(endpoint: impl Into, account: impl Into) -> Self { + Self::Foundry(FoundryAiBinding { + endpoint: endpoint.into(), + account: account.into(), + }) + } + + 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_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..9b44528c9 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,9 @@ mod storage; mod vault; mod worker; +pub use ai::{ + AiBinding, BedrockAiBinding, ExternalAiBinding, FoundryAiBinding, 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..ef69679fa 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, @@ -62,6 +62,7 @@ mod schema_snapshots { #[test] fn import_data_schema_snapshot() { let schemas = IndexMap::from([ + ("aws_ai", schema::()), ( "aws_artifact_registry", schema::(), @@ -88,6 +89,7 @@ mod schema_snapshots { ), ("aws_storage", schema::()), ("aws_vault", schema::()), + ("azure_ai", schema::()), ( "azure_artifact_registry", schema::(), @@ -135,6 +137,7 @@ mod schema_snapshots { schema::(), ), ("azure_vault", schema::()), + ("gcp_ai", schema::()), ( "gcp_artifact_registry", schema::(), diff --git a/crates/alien-core/src/import/data/snapshots/alien_core__import__data__schema_snapshots__import_data_schemas.snap b/crates/alien-core/src/import/data/snapshots/alien_core__import__data__schema_snapshots__import_data_schemas.snap index 85579d058..2054632d4 100644 --- a/crates/alien-core/src/import/data/snapshots/alien_core__import__data__schema_snapshots__import_data_schemas.snap +++ b/crates/alien-core/src/import/data/snapshots/alien_core__import__data__schema_snapshots__import_data_schemas.snap @@ -3,6 +3,21 @@ source: crates/alien-core/src/import/data/mod.rs expression: schemas --- { + "aws_ai": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "AWS AI ImportData.", + "properties": { + "region": { + "description": "AWS region where Bedrock is accessed.", + "type": "string" + } + }, + "required": [ + "region" + ], + "title": "AwsAiImportData", + "type": "object" + }, "aws_artifact_registry": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "AWS ArtifactRegistry ImportData.", @@ -605,6 +620,36 @@ expression: schemas "title": "AwsVaultImportData", "type": "object" }, + "azure_ai": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Azure AI (AIServices) ImportData.\n\nCarries 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.", + "properties": { + "accountName": { + "description": "Name of the Azure CognitiveServices / AIServices account.", + "type": "string" + }, + "endpoint": { + "description": "The endpoint URL of the AIServices account.", + "type": "string" + }, + "location": { + "description": "Azure region where the account lives (e.g. \"eastus\").", + "type": "string" + }, + "resourceGroup": { + "description": "Azure resource group containing the account.", + "type": "string" + } + }, + "required": [ + "accountName", + "endpoint", + "location", + "resourceGroup" + ], + "title": "AzureAiImportData", + "type": "object" + }, "azure_artifact_registry": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Azure ArtifactRegistry ImportData — an Azure Container Registry.", @@ -1274,6 +1319,26 @@ expression: schemas "title": "AzureVaultImportData", "type": "object" }, + "gcp_ai": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "GCP AI (Vertex AI) ImportData.\n\nThe `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.", + "properties": { + "location": { + "description": "GCP region (location) of the Vertex AI endpoint.", + "type": "string" + }, + "projectId": { + "description": "GCP project ID that owns the Vertex AI endpoint.", + "type": "string" + } + }, + "required": [ + "location", + "projectId" + ], + "title": "GcpAiImportData", + "type": "object" + }, "gcp_artifact_registry": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "GCP ArtifactRegistry ImportData — an Artifact Registry repository.", 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..a2180c94c --- /dev/null +++ b/crates/alien-core/src/resources/ai.rs @@ -0,0 +1,172 @@ +use crate::error::{ErrorData, Result}; +use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef}; +use crate::ResourceType; +use alien_error::AlienError; +use bon::Builder; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::fmt::Debug; + +/// 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. +#[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, +} + +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 { + 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(), + })); + } + 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_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-infra/src/ai/aws.rs b/crates/alien-infra/src/ai/aws.rs new file mode 100644 index 000000000..d3953c830 --- /dev/null +++ b/crates/alien-infra/src/ai/aws.rs @@ -0,0 +1,189 @@ +use std::time::Duration; + +use tracing::info; + +use crate::core::{ResourceControllerContext, ResourcePermissionsHelper}; +use crate::error::{ErrorData, Result}; +use alien_core::{ + bindings::AiBinding, Ai, AiHeartbeatData, AiHeartbeatStatus, AiOutputs, + AwsBedrockAiHeartbeatData, HeartbeatBackend, Platform, ResourceHeartbeat, + ResourceHeartbeatData, ResourceOutputs, ResourceStatus, +}; +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, +} + +#[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"); + + 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 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, "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), + }; + Ok(Some( + serde_json::to_value(AiBinding::bedrock(region)) + .into_alien_error() + .context(ErrorData::ResourceStateSerializationFailed { + resource_id: "binding".to_string(), + message: "Failed to serialize AI binding parameters".to_string(), + })?, + )) + } +} 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..ccc8e2269 --- /dev/null +++ b/crates/alien-infra/src/ai/aws_import.rs @@ -0,0 +1,31 @@ +//! 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), + _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..2859cb79b --- /dev/null +++ b/crates/alien-infra/src/ai/azure.rs @@ -0,0 +1,998 @@ +use tracing::info; + +use crate::azure_utils::get_resource_group_name; +use crate::core::{ResourceControllerContext, ResourcePermissionsHelper}; +use crate::error::{ErrorData, Result}; +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, Ai, AiHeartbeatData, AiHeartbeatStatus, AiOutputs, + AzureFoundryAiHeartbeatData, HeartbeatBackend, Platform, ResourceHeartbeat, + ResourceHeartbeatData, ResourceOutputs, ResourceStatus, +}; +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, +} + +#[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"); + 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), + }; + Ok(Some( + serde_json::to_value(AiBinding::foundry(endpoint, account_name)) + .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; + } + + /// 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()), + _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()), + _internal_stay_count: None, + } + } +} + +#[cfg(all(test, feature = "test-utils"))] +mod tests { + use super::*; + use crate::core::controller_test::SingleControllerExecutor; + 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 alien_core::{Ai, AiOutputs, Platform, ResourceStatus}; + use alien_error::AlienError; + use std::sync::Arc; + + fn basic_ai() -> Ai { + Ai::new("my-ai".to_string()).build() + } + + /// 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); + } +} 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..1c625dc5d --- /dev/null +++ b/crates/alien-infra/src/ai/azure_import.rs @@ -0,0 +1,35 @@ +//! 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), + _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..278e451ea --- /dev/null +++ b/crates/alien-infra/src/ai/gcp.rs @@ -0,0 +1,335 @@ +use std::time::Duration; + +use tracing::info; + +use crate::core::{ResourceControllerContext, ResourcePermissionsHelper}; +use crate::error::{ErrorData, Result}; +use alien_core::{ + bindings::AiBinding, Ai, AiHeartbeatData, AiHeartbeatStatus, AiOutputs, + GcpVertexAiHeartbeatData, HeartbeatBackend, Platform, ResourceHeartbeat, ResourceHeartbeatData, + ResourceOutputs, ResourceStatus, +}; +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, +} + +#[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?; + + 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); + }; + Ok(Some( + serde_json::to_value(AiBinding::vertex(project, location)) + .into_alien_error() + .context(ErrorData::ResourceStateSerializationFailed { + resource_id: "binding".to_string(), + message: "Failed to serialize AI binding parameters".to_string(), + })?, + )) + } +} 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..0de6725b7 --- /dev/null +++ b/crates/alien-infra/src/ai/gcp_import.rs @@ -0,0 +1,36 @@ +//! 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), + _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..2219bd2b9 100644 --- a/crates/alien-infra/src/core/service_provider.rs +++ b/crates/alien-infra/src/core/service_provider.rs @@ -46,6 +46,7 @@ use alien_azure_clients::{ AzureServiceBusDataPlaneClient, AzureServiceBusManagementClient, ServiceBusDataPlaneApi, ServiceBusManagementApi, }, + cognitive_services::{AzureCognitiveServicesClient, CognitiveServicesAccountsApi}, storage_accounts::{AzureStorageAccountsClient, StorageAccountsApi}, tables::{AzureTableManagementClient, TableManagementApi}, AzureClientConfig, AzureTokenCache, @@ -225,6 +226,10 @@ pub trait PlatformServiceProvider: Send + Sync { &self, config: &AzureClientConfig, ) -> Result>; + fn get_azure_cognitive_services_client( + &self, + config: &AzureClientConfig, + ) -> Result>; fn get_azure_key_vault_management_client( &self, config: &AzureClientConfig, @@ -948,6 +953,16 @@ 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_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 db5b9e64e..ca93b4e02 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, @@ -866,11 +867,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, @@ -893,6 +1022,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, @@ -914,6 +1044,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/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..43e31b21e 100644 --- a/crates/alien-permissions/tests/registry_tests.rs +++ b/crates/alien-permissions/tests/registry_tests.rs @@ -1,5 +1,203 @@ 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_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"] { + 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 4fccb9acc..8e6dbef10 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-preflights/src/mutations/gcp_service_activation.rs b/crates/alien-preflights/src/mutations/gcp_service_activation.rs index f51c30263..b78f24164 100644 --- a/crates/alien-preflights/src/mutations/gcp_service_activation.rs +++ b/crates/alien-preflights/src/mutations/gcp_service_activation.rs @@ -20,6 +20,7 @@ use tracing::{debug, info}; /// - role: iam.googleapis.com + cloudresourcemanager.googleapis.com /// - artifact-registry: artifactregistry.googleapis.com /// - kv: firestore.googleapis.com (Firestore) +/// - ai: aiplatform.googleapis.com (Vertex AI) /// - queue: pubsub.googleapis.com (Pub/Sub) /// - vault: secretmanager.googleapis.com (Secret Manager) /// - postgres: sqladmin.googleapis.com (Cloud SQL) + compute.googleapis.com (PSC) + secretmanager.googleapis.com (connection secret) @@ -159,6 +160,15 @@ impl GcpServiceActivationMutation { "firestore.googleapis.com".to_string(), ); } + "ai" => { + // Vertex AI. The native controller enables this at runtime, but a + // Frozen/Terraform GCP deploy relies on this activation being injected + // here, otherwise the first Vertex call fails with SERVICE_DISABLED. + services.insert( + "enable-aiplatform".to_string(), + "aiplatform.googleapis.com".to_string(), + ); + } "queue" => { services.insert( "enable-pubsub".to_string(), @@ -231,3 +241,22 @@ impl GcpServiceActivationMutation { services } } + +#[cfg(test)] +mod tests { + use super::*; + use alien_core::{Ai, ResourceLifecycle, Stack}; + + #[test] + fn ai_resource_requires_vertex_aiplatform() { + let stack = Stack::new("test-stack".to_string()) + .add(Ai::new("llm".to_string()).build(), ResourceLifecycle::Frozen) + .build(); + let services = GcpServiceActivationMutation.get_required_services(&stack); + assert_eq!( + services.get("enable-aiplatform").map(String::as_str), + Some("aiplatform.googleapis.com"), + "a GCP AI resource must inject Vertex AI enablement for the Terraform/Frozen path" + ); + } +} 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..78888111c --- /dev/null +++ b/crates/alien-terraform/src/emitters/aws/ai.rs @@ -0,0 +1,119 @@ +//! 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 `ai/invoke` 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::{ + emitter::{TfEmitter, TfFragment}, + emitters::aws::helpers::{ + aws_terraform_permission_context, downcast, emit_iam_role_policy_for_target_with_label, + iam_policy_name_sanitize, required_label, + }, + expr, +}; +use alien_core::{ + import::EmitContext, Ai, PermissionProfile, PermissionSetReference, Result, ServiceAccount, +}; +use alien_permissions::BindingTarget; +use hcl::expr::Expression; + +#[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, + )?; + } + } + } + + 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() +} 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..a18e20f5f 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,7 +7,7 @@ use super::helpers::{assert_terraform_valid, render, snapshot_module}; use alien_core::{ - Kv, LifecycleRule, PermissionProfile, Queue, ResourceLifecycle, ServiceAccount, Stack, + Ai, Kv, LifecycleRule, PermissionProfile, Queue, ResourceLifecycle, ServiceAccount, Stack, StackSettings, Storage, Vault, }; use alien_terraform::TerraformTarget; @@ -148,3 +148,58 @@ 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"); +} 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..e5b277724 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,155 @@ 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`, `models`, and `results` 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, + models: Option>, + results: Option>, +} + +/// One entry in the availability-filtered model list, with its picker metadata. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AiModelInfo { + id: String, + provider: String, + display_name: String, +} + +/// The outcome of invoking one listed model. `ok` is true on a completion or a 429 +/// (enabled but rate-limited); false means the model was listed but not invocable. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AiInvokeResult { + #[allow(dead_code)] + model: String, + ok: bool, + #[serde(default)] + detail: 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 config. The second exercises the availability contract: +/// getAvailableModels is filtered to the models this cloud actually has enabled, and +/// every listed model is invoked through the embedded gateway under the workload's +/// ambient credentials. A 429 counts as available (enabled but rate-limited), so this +/// passes on a quota-zeroed account; a 403/404 on a listed model would mean the +/// filter is broken. Set ALIEN_E2E_AI_SKIP_INVOKE=1 to run provisioning-only. +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(180)) + .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!("getAvailableModels returned no models; expected at least one enabled model"); + } + + // Every listed model must carry picker metadata (provider + displayName). + let models = data.models.unwrap_or_default(); + if models.len() != model_count { + bail!("modelCount {model_count} disagrees with the models list ({})", models.len()); + } + for m in &models { + if m.provider.is_empty() || m.display_name.is_empty() { + bail!("listed model '{}' is missing provider/displayName enrichment", m.id); + } + } + + // Every listed model must be invocable (2xx or 429). The list is + // availability-filtered, so a 403/404 on a listed model means the filter let a + // disabled model through. + let results = data.results.unwrap_or_default(); + if results.len() != model_count { + bail!("modelCount {model_count} disagrees with the results list ({})", results.len()); + } + let failed: Vec<&AiInvokeResult> = results.iter().filter(|r| !r.ok).collect(); + if !failed.is_empty() { + bail!( + "some listed models were not invocable (a listed-but-unavailable model means the availability filter is broken): {failed:?}" + ); + } + + info!( + model_count, + "AI availability check passed: every enabled model listed (with provider/displayName) invoked cleanly (2xx or 429)" + ); + 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/ai-quickstart-ts/README.md b/examples/ai-quickstart-ts/README.md new file mode 100644 index 000000000..26c5e43b8 --- /dev/null +++ b/examples/ai-quickstart-ts/README.md @@ -0,0 +1,43 @@ +# 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 only the models this cloud actually has enabled, each with an `id`, +`provider`, and `displayName`, so you can use it as a picker. `/ask` uses `?model=`, +or the first available model. + +## Model availability + +`getAvailableModels()` returns what is enabled on your deployment's cloud right now. +Some models need a one-time activation in the cloud provider before they appear: + +- **AWS Bedrock**: open-weight models (GPT-OSS, Llama, Mistral, Qwen, and more) work + out of the box. Claude needs the one-time Anthropic use-case form in the Bedrock + console. +- **GCP Vertex**: Gemini works out of the box. Claude needs enabling in Vertex AI + Model Garden with Anthropic's terms accepted (Google Cloud console). +- **Azure AI Foundry**: the GPT models are deployed for you. Claude needs a one-time + Marketplace-terms acceptance and deployment in the Foundry portal. + +Until you complete a model's activation step it simply will not appear in +`getAvailableModels()`. Your deployment still succeeds and every other model keeps +working. diff --git a/examples/ai-quickstart-ts/alien.ts b/examples/ai-quickstart-ts/alien.ts new file mode 100644 index 000000000..652ce109c --- /dev/null +++ b/examples/ai-quickstart-ts/alien.ts @@ -0,0 +1,24 @@ +import * as alien from "@alienplatform/core" + +// An AI model the app can call, served by the customer's own cloud; no API keys. +const assistant = new alien.AI("assistant").build() + +const api = new alien.Worker("api") + .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) + .publicEndpoint("api") + .link(assistant) + .permissions("execution") + .build() + +export default new alien.Stack("ai-quickstart") + .platforms(["aws", "gcp", "azure"]) + .add(assistant, "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..67f272208 --- /dev/null +++ b/examples/ai-quickstart-ts/src/index.ts @@ -0,0 +1,33 @@ +import { ai } from "@alienplatform/sdk" +import { Hono } from "hono" + +const app = new Hono() + +// The models this deployment's cloud actually has enabled, each with an id, +// provider, and displayName for a picker. Models not enabled on this cloud are +// simply absent, so call this to discover what you can use. +app.get("/models", async c => { + const models = await ai("assistant").getAvailableModels() + return c.json({ models }) +}) + +// One-shot question -> answer. Discover then pick: `?model=` overrides, else use the +// first model getAvailableModels returned for this cloud. +app.get("/ask", async c => { + const question = c.req.query("q") + if (!question) { + return c.json({ error: "pass a question as ?q=..." }, 400) + } + const assistant = ai("assistant") + const model = c.req.query("model") ?? (await assistant.getAvailableModels())[0]?.id + if (!model) { + return c.json({ error: "no models available for this cloud" }, 500) + } + const completion = await assistant.chat.completions.create({ + model, + messages: [{ role: "user", content: question }], + }) + 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..4688b3b49 100644 --- a/examples/pnpm-lock.yaml +++ b/examples/pnpm-lock.yaml @@ -11,11 +11,31 @@ 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-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 +53,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 +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) byoc-database: devDependencies: @@ -76,7 +96,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 +187,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 +205,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 +233,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 +363,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 +419,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 +447,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 +509,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 +561,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 +601,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 +1196,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 +1369,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 +1421,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 +1433,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 +1445,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 +1513,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 +1690,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 +1705,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 +2013,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 +2609,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 +2863,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 +2896,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 +2933,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 +2942,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 +2953,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 +2962,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 +2984,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 +3004,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 + optional: true + + '@aws-sdk/token-providers@3.1092.0': + dependencies: + '@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/types@3.973.5': + '@aws-sdk/types@3.974.2': dependencies: - '@smithy/types': 4.13.0 + '@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 +3205,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 +3214,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 +3806,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 +3913,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 +3957,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 +3974,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 +3996,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 +4006,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 +4020,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 +4086,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 +4128,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 +4157,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 +4180,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 +4195,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 +4444,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 +4458,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 +4822,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 +5180,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 +5199,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 +5504,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.2.0: - optional: true - stubs@3.0.0: optional: true @@ -5705,7 +5657,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 +5683,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 +5699,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 +5725,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..70571183b 100644 --- a/examples/pnpm-workspace.yaml +++ b/examples/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - ai-quickstart-ts - basic-worker-ts - basic-worker-rs - remote-worker-ts diff --git a/packages/ai-gateway/package.json b/packages/ai-gateway/package.json new file mode 100644 index 000000000..62dc2260d --- /dev/null +++ b/packages/ai-gateway/package.json @@ -0,0 +1,56 @@ +{ + "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", + "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"], + "author": "Alien Software, Inc. ", + "license": "FSL-1.1-Apache-2.0", + "description": "Thin TypeScript wrapper that spawns the Alien AI gateway (a Rust binary) and points an OpenAI-compatible client at it", + "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", + "@types/node": "^24.0.15", + "openai": "^4", + "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/generate-prebuilds.mjs b/packages/ai-gateway/scripts/generate-prebuilds.mjs new file mode 100644 index 000000000..80209ac26 --- /dev/null +++ b/packages/ai-gateway/scripts/generate-prebuilds.mjs @@ -0,0 +1,50 @@ +/** + * Generate the per-platform prebuild package manifests + * (`packages/ai-gateway/npm//package.json`) at the wrapper's version. + * + * Each prebuild package carries one file: the `alien-ai-gateway` launcher + * binary for that platform, which `@alienplatform/ai-gateway` resolves and + * spawns. The manifests are generated by the release pipeline rather than + * checked in: every field is derived (the name, the wrapper's version, and the + * platform selectors npm uses to pick the matching optional dependency), so a + * checked-in copy would only add version-lockstep churn. + * + * The linux binaries are static musl builds, so a triple's `gnu` and `musl` + * packages ship the same executable under two names: npm's `libc` selector + * refuses to install a `glibc`-tagged package on Alpine, so both spellings + * must exist for the loader's `platformTriple()` to resolve on either libc. + */ + +import { mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { join, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const TRIPLES = [ + { triple: "darwin-arm64", os: "darwin", cpu: "arm64" }, + { triple: "darwin-x64", os: "darwin", cpu: "x64" }, + { triple: "linux-x64-gnu", os: "linux", cpu: "x64", libc: "glibc" }, + { triple: "linux-x64-musl", os: "linux", cpu: "x64", libc: "musl" }, + { triple: "linux-arm64-gnu", os: "linux", cpu: "arm64", libc: "glibc" }, + { triple: "linux-arm64-musl", os: "linux", cpu: "arm64", libc: "musl" }, +] + +const pkgDir = process.argv[2] + ? resolve(process.argv[2]) + : fileURLToPath(new URL("..", import.meta.url)) +const { version } = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8")) + +for (const { triple, os, cpu, libc } of TRIPLES) { + const dir = join(pkgDir, "npm", triple) + mkdirSync(dir, { recursive: true }) + const manifest = { + name: `@alienplatform/ai-gateway-${triple}`, + version, + description: `The alien-ai-gateway launcher binary for ${triple}, resolved and spawned by @alienplatform/ai-gateway`, + os: [os], + cpu: [cpu], + ...(libc ? { libc: [libc] } : {}), + files: ["alien-ai-gateway"], + } + writeFileSync(join(dir, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`) + console.log(`Generated npm/${triple}/package.json at ${version}`) +} 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..25a2336f7 --- /dev/null +++ b/packages/ai-gateway/scripts/inject-optional-deps.mjs @@ -0,0 +1,52 @@ +/** + * 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 launcher binary. + * + * The release pipeline runs this after `generate-prebuilds.mjs` has written the + * per-platform manifests and before publishing the wrapper. An explicit, exact + * pin is deterministic and reviewable. + */ + +import { readFileSync, readdirSync, writeFileSync } from "node:fs" +import { fileURLToPath } from "node:url" + +const TRIPLES = [ + "darwin-arm64", + "darwin-x64", + "linux-x64-gnu", + "linux-x64-musl", + "linux-arm64-gnu", + "linux-arm64-musl", +] + +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 generate-prebuilds.mjs wrote. +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..852fae100 --- /dev/null +++ b/packages/ai-gateway/scripts/validate-release-version.mjs @@ -0,0 +1,29 @@ +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)), "../../..") +// The wrapper package. Per-platform binary prebuild manifests +// (packages/ai-gateway/npm//package.json) are generated + version-stamped +// by the release pipeline, so they are validated there rather than pinned here. +const manifests = ["packages/ai-gateway/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}`) +} + +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..db9f381b5 --- /dev/null +++ b/packages/ai-gateway/src/__tests__/client.test.ts @@ -0,0 +1,362 @@ +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("resolves a BYO anthropic provider to the Anthropic API", async () => { + vi.stubEnv( + "ALIEN_LLM_BINDING", + JSON.stringify({ service: "external", provider: "anthropic", apiKey: "sk-ant" }), + ) + expect((await getAiConnection("llm")).baseURL).toBe("https://api.anthropic.com/v1") + }) + + it("fails closed on an unknown BYO provider rather than shipping the key to OpenAI", async () => { + // The projected key is attached as a bearer token, so an unlisted provider must error + // instead of defaulting to OpenAI's endpoint. + vi.stubEnv( + "ALIEN_LLM_BINDING", + JSON.stringify({ service: "external", provider: "google", apiKey: "sk-test" }), + ) + await expect(getAiConnection("llm")).rejects.toMatchObject({ + code: "AI_UNSUPPORTED_PROVIDER", + httpStatusCode: 400, + }) + }) + + it("lets ALIEN_AI_LOCAL_BASE_URL override an otherwise-unknown provider", async () => { + vi.stubEnv( + "ALIEN_LLM_BINDING", + JSON.stringify({ service: "external", provider: "google", apiKey: "sk-test" }), + ) + vi.stubEnv("ALIEN_AI_LOCAL_BASE_URL", "http://localhost:8080") + expect((await getAiConnection("llm")).baseURL).toBe("http://localhost:8080/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("returns current-generation Anthropic ids for a BYO anthropic provider, no retired 3.5", async () => { + vi.stubEnv( + "ALIEN_LLM_BINDING", + JSON.stringify({ service: "external", provider: "anthropic", apiKey: "sk-ant" }), + ) + const fetchMock = stubFetch({ data: [] }) + const models = await ai("llm").getAvailableModels() + const ids = models.map(m => m.id) + expect(ids).toContain("claude-opus-4-8") + expect(ids).toContain("claude-sonnet-5") + expect(ids).toContain("claude-haiku-4-5") + expect(ids.some(id => id.startsWith("claude-3-5"))).toBe(false) + // Each entry carries the provider/displayName shape a model picker consumes. + expect(models[0]).toMatchObject({ provider: "anthropic", displayName: models[0]!.id }) + 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", provider: "openai", displayName: "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", provider: "openai", displayName: "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", provider: "openai", displayName: "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", provider: "openai", displayName: "GPT-OSS 20B" }, + ]) + expect(start).toHaveBeenCalledTimes(2) + }) +}) + +describe("Ai.responses.create", () => { + const ANTHROPIC_BYO = JSON.stringify({ + service: "external", + provider: "anthropic", + apiKey: "sk-ant", + }) + + it("fails fast on a BYO-Anthropic Responses call (its compat host has no /v1/responses)", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", ANTHROPIC_BYO) + const fetchMock = stubFetch({ id: "resp", output: [] }) + await expect( + ai("llm").responses.create({ model: "claude-opus-4-8", input: "hi" }), + ).rejects.toMatchObject({ code: "AI_RESPONSES_API_UNSUPPORTED", httpStatusCode: 400 }) + // The reason is known before the request, so we never hit the 404. + expect(fetchMock).not.toHaveBeenCalled() + }) + + it("still serves chat.completions for a BYO-Anthropic binding (only Responses is blocked)", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", ANTHROPIC_BYO) + const fetchMock = stubFetch({ id: "x", choices: [] }) + await ai("llm").chat.completions.create({ model: "claude-opus-4-8", messages: [] }) + expect(callUrl(fetchMock)).toBe("https://api.anthropic.com/v1/chat/completions") + }) + + it("lets a BYO-Anthropic Responses call through when ALIEN_AI_LOCAL_BASE_URL overrides the base", async () => { + vi.stubEnv("ALIEN_LLM_BINDING", ANTHROPIC_BYO) + vi.stubEnv("ALIEN_AI_LOCAL_BASE_URL", "http://localhost:11434") + const fetchMock = stubFetch({ id: "resp", output: [] }) + await ai("llm").responses.create({ model: "claude-opus-4-8", input: "hi" }) + expect(callUrl(fetchMock)).toBe("http://localhost:11434/v1/responses") + }) + + 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, + ) + }) +}) 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..568773485 --- /dev/null +++ b/packages/ai-gateway/src/__tests__/gateway.test.ts @@ -0,0 +1,135 @@ +import { EventEmitter } from "node:events" +import { AlienError } from "@alienplatform/core" +import { afterEach, describe, expect, it, vi } from "vitest" + +import { createGateway } from "../gateway.js" + +// `vi.hoisted` so the (hoisted) `vi.mock` factory can reference the mock. +const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() })) +vi.mock("node:child_process", () => ({ spawn: (...args: unknown[]) => spawnMock(...args) })) + +/** A minimal stand-in for a spawned ChildProcess the gateway wrapper drives. Real stdio are + * `net.Socket`s, so the streams carry `unref` alongside `resume`. */ +type FakeStream = EventEmitter & { resume: () => void; unref: () => void } +function fakeChild() { + const mkStream = (): FakeStream => { + const s = new EventEmitter() as FakeStream + s.resume = vi.fn() + s.unref = vi.fn() + return s + } + const child = new EventEmitter() as EventEmitter & { + stdout: FakeStream + stderr: FakeStream + kill: () => void + unref: () => void + } + child.stdout = mkStream() + child.stderr = mkStream() + child.kill = vi.fn() + child.unref = vi.fn() + return child +} + +const READY = '{"aiGatewayUrl":"http://127.0.0.1:41999"}\n' + +afterEach(() => { + spawnMock.mockReset() + vi.unstubAllEnvs() +}) + +describe("createGateway", () => { + it("spawns the binary once, reads the printed URL, and reuses the handle", async () => { + spawnMock.mockImplementation(() => { + const child = fakeChild() + // Emit after the wrapper has attached its listeners (next microtask). + queueMicrotask(() => child.stdout.emit("data", Buffer.from(READY))) + return child + }) + const gateway = createGateway(async () => "/opt/alien-ai-gateway") + + expect(await gateway.startAiGateway()).toEqual({ url: "http://127.0.0.1:41999" }) + expect(await gateway.startAiGateway()).toEqual({ url: "http://127.0.0.1:41999" }) + expect(spawnMock).toHaveBeenCalledTimes(1) + expect(spawnMock).toHaveBeenCalledWith("/opt/alien-ai-gateway", ["--gateway-serve"], { + stdio: ["ignore", "pipe", "pipe"], + }) + }) + + it("unrefs the child and both stdio sockets so a one-shot host can exit", async () => { + let child: ReturnType | undefined + spawnMock.mockImplementation(() => { + child = fakeChild() + queueMicrotask(() => child?.stdout.emit("data", Buffer.from(READY))) + return child + }) + const gateway = createGateway(async () => "/opt/alien-ai-gateway") + + await gateway.startAiGateway() + + // resume() drains the pipes but keeps their sockets referenced. Unref-ing the child + // alone is not enough: unless both stdio sockets are unref'd too, a process that calls + // ai() once and returns never exits. (Manual end-to-end check: a one-shot script that + // awaits a single ai() call should exit on its own, with no lingering handle.) + expect(child?.stdout.resume).toHaveBeenCalledTimes(1) + expect(child?.stderr.resume).toHaveBeenCalledTimes(1) + expect(child?.stdout.unref).toHaveBeenCalledTimes(1) + expect(child?.stderr.unref).toHaveBeenCalledTimes(1) + expect(child?.unref).toHaveBeenCalledTimes(1) + }) + + it("uses ALIEN_AI_GATEWAY_URL without spawning when a launcher already started the gateway", async () => { + vi.stubEnv("ALIEN_AI_GATEWAY_URL", "http://127.0.0.1:9008") + const gateway = createGateway(async () => "/opt/alien-ai-gateway") + + expect(await gateway.startAiGateway()).toEqual({ url: "http://127.0.0.1:9008" }) + expect(spawnMock).not.toHaveBeenCalled() + }) + + it("retries after a transient startup failure instead of caching the rejection", async () => { + spawnMock + .mockImplementationOnce(() => { + const child = fakeChild() + // Exit before printing a URL, with a reason on stderr. + queueMicrotask(() => { + child.stderr.emit("data", Buffer.from("ambient credential not ready")) + child.emit("exit", 1, null) + }) + return child + }) + .mockImplementationOnce(() => { + const child = fakeChild() + queueMicrotask(() => child.stdout.emit("data", Buffer.from(READY))) + return child + }) + const gateway = createGateway(async () => "/opt/alien-ai-gateway") + + await expect(gateway.startAiGateway()).rejects.toThrow(AlienError) + // A cached rejection would leave the gateway permanently dead for this process. + expect(await gateway.startAiGateway()).toEqual({ url: "http://127.0.0.1:41999" }) + expect(spawnMock).toHaveBeenCalledTimes(2) + }) + + it("surfaces the child's stderr as the startup failure reason", async () => { + spawnMock.mockImplementation(() => { + const child = fakeChild() + queueMicrotask(() => { + child.stderr.emit("data", Buffer.from("bind: address already in use")) + child.emit("exit", 1, null) + }) + return child + }) + const gateway = createGateway(async () => "/opt/alien-ai-gateway") + + await expect(gateway.startAiGateway()).rejects.toThrow(/address already in use/) + }) + + it("rejects rather than throwing synchronously when the binary cannot be resolved", async () => { + const gateway = createGateway(async () => { + throw new Error("no alien-ai-gateway binary for this platform") + }) + // A synchronous throw would escape a caller's `.catch()`. + await expect(gateway.startAiGateway()).rejects.toThrow() + expect(spawnMock).not.toHaveBeenCalled() + }) +}) 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..e63de77d6 --- /dev/null +++ b/packages/ai-gateway/src/__tests__/loader.test.ts @@ -0,0 +1,53 @@ +import { existsSync, readFileSync } from "node:fs" +import { afterEach, describe, expect, it, vi } from "vitest" + +import { + platformTriple, + registerEmbeddedBinary, + resetGatewayBinaryCacheForTests, + resolveGatewayBinary, +} from "../loader.js" + +afterEach(() => { + resetGatewayBinaryCacheForTests() + vi.unstubAllEnvs() + ;(globalThis as { Bun?: unknown }).Bun = undefined +}) + +describe("platformTriple", () => { + it("maps known platforms, including musl (the binary supports Alpine)", () => { + expect(platformTriple("darwin", "arm64")).toBe("darwin-arm64") + expect(platformTriple("darwin", "x64")).toBe("darwin-x64") + expect(platformTriple("linux", "x64", "gnu")).toBe("linux-x64-gnu") + // The napi addon rejected musl; the standalone binary supports it. + expect(platformTriple("linux", "arm64", "musl")).toBe("linux-arm64-musl") + }) + + it("throws on an unsupported platform", () => { + expect(() => platformTriple("sunos" as NodeJS.Platform, "x64")).toThrow() + }) +}) + +describe("resolveGatewayBinary", () => { + it("uses ALIEN_AI_GATEWAY_BINARY_PATH when set", async () => { + vi.stubEnv("ALIEN_AI_GATEWAY_BINARY_PATH", "/custom/alien-ai-gateway") + expect(await resolveGatewayBinary()).toBe("/custom/alien-ai-gateway") + }) + + it("extracts a registered embedded binary to a runnable file (compiled Worker path)", async () => { + const contents = "#!/bin/sh\nexit 0\n" + const bytes = new TextEncoder().encode(contents) + // A `bun build --compile` binary registers its embedded copy; the loader must + // prefer it and extract it to a real, executable file (the embedded path is + // virtual). Stub the Bun runtime the extractor uses. + ;(globalThis as { Bun?: unknown }).Bun = { + file: () => ({ arrayBuffer: async () => bytes.buffer }), + } + registerEmbeddedBinary("/$bunfs/root/alien-ai-gateway.bin") + + const extracted = await resolveGatewayBinary() + expect(extracted).toMatch(/alien-ai-gateway$/) + expect(existsSync(extracted)).toBe(true) + expect(readFileSync(extracted, "utf8")).toBe(contents) + }) +}) diff --git a/packages/ai-gateway/src/assets.d.ts b/packages/ai-gateway/src/assets.d.ts new file mode 100644 index 000000000..70c091994 --- /dev/null +++ b/packages/ai-gateway/src/assets.d.ts @@ -0,0 +1,7 @@ +// A `with { type: "file" }` import of the embedded launcher binary resolves to its +// path string. `bun build --compile` embeds the file and rewrites the path to the +// in-binary copy; `native.ts` hands that path to the loader to extract and spawn. +declare module "*.bin" { + const path: string + export default path +} 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..eefd3a2c8 --- /dev/null +++ b/packages/ai-gateway/src/client.ts @@ -0,0 +1,486 @@ +/** + * AI binding — a thin OpenAI-compatible client to the workload's AI gateway. + * + * For an ambient-cloud binding the gateway process 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" +// Types only (erased at build): OpenAI's canonical result shapes, so callers get +// a typed completion without reinventing them. `@alienplatform/ai-gateway` keeps +// `openai` a devDependency; its runtime dependency stays `@alienplatform/core`. +import type { ChatCompletion, ChatCompletionChunk } from "openai/resources/chat/completions" +import type { + Response as OpenAIResponse, + ResponseStreamEvent, +} from "openai/resources/responses/responses" + +import { isExternalAiBinding, parseAiBinding } from "./binding.js" +import { + AiTransportError, + AiUpstreamError, + BindingNotFoundError, + ResponsesApiUnsupportedError, + UnsupportedProviderError, +} 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 { + /** The id passed to `chat.completions.create` / `responses.create`. */ + id: string + /** The model's publisher, e.g. "openai", "anthropic", "google". */ + provider: string + /** A human label for a model picker, e.g. "Claude Opus 4.8". */ + displayName: string +} + +/** + * OpenAI-compatible chat-completions surface, typed so a non-streaming call + * resolves to a full {@link ChatCompletion} and a streaming call to an + * async-iterable of {@link ChatCompletionChunk}; the caller reads + * `.choices[0].message.content` (or iterates chunks) with no cast. + */ +export interface ChatCompletionsApi { + create(params: ChatCompletionCreateParams & { stream?: false }): Promise + create( + params: ChatCompletionCreateParams & { stream: true }, + ): Promise> +} + +/** OpenAI-compatible Responses surface (the API Codex speaks), typed like above. */ +export interface ResponsesApi { + create(params: ResponseCreateParams & { stream?: false }): Promise + create( + params: ResponseCreateParams & { stream: true }, + ): Promise> +} + +// The BYO-key providers we know the upstream base URL for. `_postSurface` attaches the +// projected key as `Authorization: Bearer`, so this map is a security boundary, not a +// convenience default: resolving an unlisted provider to some fallback host would ship the +// customer's key to the wrong provider. +const KNOWN_PROVIDER_BASE_URLS: Record = { + openai: "https://api.openai.com", + anthropic: "https://api.anthropic.com", +} + +// Upstream base URL (no `/v1`) for a BYO-key provider. `ALIEN_AI_LOCAL_BASE_URL` overrides it so +// any OpenAI-compatible provider works locally; without an override we fail closed on an unknown +// provider rather than defaulting it to OpenAI and leaking the key there. +function providerBaseUrl(provider: string): string { + const override = process.env.ALIEN_AI_LOCAL_BASE_URL + if (override) return override.replace(/\/$/, "") + const base = KNOWN_PROVIDER_BASE_URLS[provider] + if (!base) { + throw new AlienError( + UnsupportedProviderError.create({ + provider, + supported: Object.keys(KNOWN_PROVIDER_BASE_URLS), + }), + ) + } + return base +} + +// 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). The Anthropic ids are the current- +// generation aliases served by api.anthropic.com. +function defaultModels(provider: string): string[] { + return provider === "anthropic" + ? ["claude-opus-4-8", "claude-sonnet-5", "claude-haiku-4-5"] + : ["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?: AiModel[] +} + +/** + * 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, + // BYO and ambient bindings return the identical AiModel shape; a BYO id is + // its own display label since we do not curate the provider's own catalog. + staticModels: defaultModels(binding.provider).map(id => ({ + id, + provider: binding.provider, + displayName: id, + })), + } + } + 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: ChatCompletionsApi } + + /** OpenAI-compatible Responses API namespace (the surface Codex speaks). */ + readonly responses: ResponsesApi + + constructor(resolve: () => Promise) { + this.resolve = resolve + + // One internal cast bridges the single passthrough impl to the typed + // stream/non-stream overloads, so callers never cast the result themselves. + this.chat = { + completions: { + create: ((params: ChatCompletionCreateParams) => + this._chatCompletionsCreate(params)) as ChatCompletionsApi["create"], + }, + } + + this.responses = { + create: ((params: ResponseCreateParams) => + this._responsesCreate(params)) as ResponsesApi["create"], + } + } + + // 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 + } + 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") + } + // The gateway subprocess is a separate trust boundary (it can be a stale or + // mismatched binary), so verify each element actually carries the AiModel + // shape instead of letting a type assertion hide missing fields. + for (const model of body.data) { + if ( + typeof model?.id !== "string" || + typeof model?.provider !== "string" || + typeof model?.displayName !== "string" + ) { + throw createUpstreamError( + url, + response.status, + `models response entry is missing id/provider/displayName: ${JSON.stringify(model)}`, + ) + } + } + return body.data + } + + private _chatCompletionsCreate(params: ChatCompletionCreateParams): Promise { + return this._postSurface("/v1/chat/completions", params) + } + + private async _responsesCreate(params: ResponseCreateParams): Promise { + // Anthropic's OpenAI-compatible host serves /v1/chat/completions but not /v1/responses, + // so a BYO-Anthropic Responses call would 404. Fail fast with the reason instead. An + // ALIEN_AI_LOCAL_BASE_URL override resolves to a different base, so it is not blocked here. + const { baseUrl } = await this.connection() + if (baseUrl === KNOWN_PROVIDER_BASE_URLS.anthropic) { + throw new AlienError(ResponsesApiUnsupportedError.create({ provider: "anthropic" })) + } + 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 +// ───────────────────────────────────────────────────────────────────────────── + +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..9ed448df3 --- /dev/null +++ b/packages/ai-gateway/src/errors.ts @@ -0,0 +1,138 @@ +/** + * 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 process, 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 a BYO-key binding names a provider the client has no base URL for. + * External (the developer set a bad `provider` tag) and not retryable — failing closed + * here stops the projected BYO key from being shipped to the wrong provider's endpoint. + */ +export const UnsupportedProviderError = defineError({ + code: "AI_UNSUPPORTED_PROVIDER", + context: z.object({ + provider: z.string(), + supported: z.array(z.string()), + }), + message: ({ provider, supported }) => + `Unknown AI provider '${provider}'. Supported providers are ${supported.join(", ")}. For any other OpenAI-compatible provider, set ALIEN_AI_LOCAL_BASE_URL to its base URL.`, + retryable: false, + internal: false, + httpStatusCode: 400, +}) + +/** + * Error thrown when a binding's upstream serves chat/completions but not the OpenAI Responses + * API. Anthropic's OpenAI-compatible host is the case in point: `/v1/chat/completions` works, + * `/v1/responses` 404s. External (a caller usage error), not retryable. + */ +export const ResponsesApiUnsupportedError = defineError({ + code: "AI_RESPONSES_API_UNSUPPORTED", + context: z.object({ + provider: z.string(), + }), + message: ({ provider }) => + `The '${provider}' provider serves chat/completions but not the OpenAI Responses API (/v1/responses). Use chat.completions.create, or set ALIEN_AI_LOCAL_BASE_URL to a Responses-capable endpoint.`, + retryable: false, + internal: false, + httpStatusCode: 400, +}) + +/** + * Error thrown when this platform/architecture has no prebuilt gateway binary. + */ +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 prebuilt binary for platform '${platform}' arch '${arch}'${reason ? `: ${reason}` : ""}.`, + retryable: false, + internal: false, + httpStatusCode: 400, +}) + +/** + * Error thrown when the `alien-ai-gateway` executable cannot be located (or, for a + * compiled Worker, extracted) for this platform. Internal because the context + * carries filesystem paths. + */ +export const GatewayBinaryUnavailableError = defineError({ + code: "AI_GATEWAY_BINARY_UNAVAILABLE", + context: z.object({ + triple: z.string(), + reason: z.string(), + path: z.string().optional(), + }), + message: ({ triple, reason }) => + `Cannot locate the alien-ai-gateway binary for '${triple}': ${reason}`, + retryable: false, + internal: true, + // Same class as UnsupportedPlatformError: the host environment can't run the binary. + httpStatusCode: 400, +}) + +/** + * Error thrown when the spawned `alien-ai-gateway` process failed to report a + * ready URL: it exited early or errored on startup. Retryable because the common + * cause (an ambient cloud credential not yet resolvable) is transient. + */ +export const GatewayStartFailedError = defineError({ + code: "AI_GATEWAY_START_FAILED", + context: z.object({ + reason: z.string(), + }), + message: ({ reason }) => `The alien-ai-gateway process failed to start: ${reason}`, + retryable: true, + internal: true, + httpStatusCode: 503, +}) diff --git a/packages/ai-gateway/src/gateway.ts b/packages/ai-gateway/src/gateway.ts new file mode 100644 index 000000000..c9bb722cb --- /dev/null +++ b/packages/ai-gateway/src/gateway.ts @@ -0,0 +1,239 @@ +/** + * The gateway surface: obtain the loopback URL of the Alien AI gateway. + * + * Two paths, unified behind {@link Gateway.startAiGateway}: + * - A container launcher already started the gateway out of band and exec'd the + * app with `ALIEN_AI_GATEWAY_URL` set, so we use that URL directly. + * - Otherwise (an SDK Worker) we spawn the `alien-ai-gateway` binary ourselves, + * read the URL it prints, and keep the child alive for the process lifetime. + * + * Parameterized over how the binary is resolved so the lazy entry (`index.ts`) and + * the compiled-embed entry (`native.ts`) share one implementation. + */ + +import { type ChildProcess, spawn } from "node:child_process" +import type { Readable } from "node:stream" +import { AlienError } from "@alienplatform/core" + +import { GatewayBinaryUnavailableError, GatewayStartFailedError } from "./errors.js" +import { type RawAiGatewayHandle, platformTriple } from "./loader.js" + +/** The env var a container launcher sets after starting the gateway out of band. */ +const URL_ENV = "ALIEN_AI_GATEWAY_URL" +/** The launcher serve mode; binds an ephemeral port and prints its URL. */ +const SERVE_FLAG = "--gateway-serve" + +export interface Gateway { + startAiGateway(): Promise +} + +export function createGateway(resolveBinary: () => Promise): Gateway { + // Started once per process: the child is held for the process lifetime, since + // killing it stops the gateway. Only a *resolved* start is memoized. Caching a + // rejection would turn one transient startup failure into a permanently dead + // gateway, even though the Rust side marks those errors retryable; and a child + // that dies after reporting ready clears the memo (see keepChildAlive), so the + // next call respawns instead of handing back a URL nothing is listening on. + let started: Promise | null = null + const forget = () => { + started = null + } + + async function startAiGateway(): Promise { + started ??= startOnce(resolveBinary, forget).catch(error => { + forget() + throw error + }) + return started + } + + return { startAiGateway } +} + +async function startOnce( + resolveBinary: () => Promise, + onGatewayLost: () => void, +): Promise { + // A launcher (container path) already started the gateway and exported its URL; + // it owns that process, so there is nothing here to keep alive or reap. + const preset = process.env[URL_ENV] + if (preset) return { url: preset } + + const binary = await resolveBinary() + const child = spawn(binary, [SERVE_FLAG], { stdio: ["ignore", "pipe", "pipe"] }) + // Track and guard the child the instant it exists, before the ready-await: a + // directed SIGTERM (or a stream I/O fault) during startup must reap it, not + // orphan it. + trackChild(child) + const url = await readReadyUrl(child, binary) + keepChildAlive(child, onGatewayLost) + return { url } +} + +/** + * Read the gateway's `{"aiGatewayUrl":"..."}` line from stdout. Rejects (with the + * child's stderr as the reason) if the process exits or errors before printing it. + */ +function readReadyUrl(child: ChildProcess, binary: string): Promise { + return new Promise((resolveUrl, rejectUrl) => { + let stdout = "" + let stderr = "" + + const cleanup = () => { + child.stdout?.off("data", onStdout) + child.stderr?.off("data", onStderr) + child.off("exit", onExit) + child.off("error", onError) + } + + const onStdout = (chunk: Buffer) => { + stdout += chunk.toString() + const url = parseReadyUrl(stdout) + if (url) { + cleanup() + resolveUrl(url) + } + } + const onStderr = (chunk: Buffer) => { + stderr += chunk.toString() + } + // An early exit is usually transient (an ambient cloud credential not yet + // resolvable), so surface it as retryable. + const onExit = (code: number | null, signal: NodeJS.Signals | null) => { + cleanup() + rejectUrl( + new AlienError( + GatewayStartFailedError.create({ + reason: + stderr.trim() || + `process exited (${signal ?? `code ${code ?? "null"}`}) before reporting a URL`, + }), + ), + ) + } + // A spawn 'error' means the OS could not exec the binary (ENOENT / EACCES / + // exec-format): the host cannot run it, so a retry against the same path is + // futile. Surface it as the non-retryable "binary unavailable" class. + const onError = async (error: Error) => { + cleanup() + // platformTriple() can throw on an unsupported host, and this runs inside an + // event callback where a throw would strand the promise; fall back to the raw + // platform/arch so the rejection always fires. + let triple: string + try { + triple = platformTriple() + } catch { + triple = `${process.platform}-${process.arch}` + } + // Chain the spawn error (ENOENT/EACCES) so its cause is preserved. + rejectUrl( + (await AlienError.from(error)).withContext( + GatewayBinaryUnavailableError.create({ + triple, + path: binary, + reason: "could not execute the gateway binary", + }), + ), + ) + } + + child.stdout?.on("data", onStdout) + child.stderr?.on("data", onStderr) + child.on("exit", onExit) + child.on("error", onError) + }) +} + +/** Extract the URL from the launcher's machine-readable stdout line, if present. */ +function parseReadyUrl(buffered: string): string | undefined { + for (const line of buffered.split("\n")) { + const trimmed = line.trim() + if (!trimmed.startsWith("{")) continue + try { + const parsed = JSON.parse(trimmed) as { aiGatewayUrl?: unknown } + if (typeof parsed.aiGatewayUrl === "string") return parsed.aiGatewayUrl + } catch { + // A partial line: wait for more stdout. + } + } + return undefined +} + +// The child currently serving the gateway. The reaper (installed once) reads this, +// so a respawn never stacks duplicate signal handlers and never leaves a previous +// child orphaned when the process is torn down. +let liveChild: ChildProcess | undefined +let reaperInstalled = false + +/** Reap whatever child is live when this process is torn down. Installed once. */ +function installReaper(): void { + if (reaperInstalled) return + reaperInstalled = true + const killLive = () => { + try { + liveChild?.kill("SIGTERM") + } catch { + // Already gone. + } + } + // Normal exit reaps synchronously. + process.once("exit", killLive) + // A directed SIGINT/SIGTERM terminates the host before 'exit' fires, which would + // orphan the child. Reap it, then re-raise the signal so the host app's own + // handlers (or the default action) still run: we don't force-exit and truncate + // its shutdown. + for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.once(signal, () => { + killLive() + process.kill(process.pid, signal) + }) + } +} + +/** + * Track a freshly spawned child as the live gateway and guard it synchronously, at + * spawn time (before the ready-await). A live child stream with no 'error' listener + * throws on any I/O fault and crashes the host, so attach permanent no-op stream + * guards now (`readReadyUrl` only adds 'data' listeners; the child itself always has + * an 'error' listener, `readReadyUrl`'s during startup then `keepChildAlive`'s after). + */ +function trackChild(child: ChildProcess): void { + liveChild = child + installReaper() + child.stdout?.on("error", () => {}) + child.stderr?.on("error", () => {}) +} + +/** + * Unref a child stdio stream's underlying handle. `stdout`/`stderr` are typed `Readable` but are + * `net.Socket` at runtime and carry `.unref()`; the guard makes a non-socket or absent stream a + * no-op instead of a crash. + */ +function unrefStream(stream: Readable | null): void { + const s = stream as (Readable & { unref?: () => void }) | null + if (s && typeof s.unref === "function") s.unref() +} + +/** + * Once the child is serving, hold it for the app's lifetime: drain its stdio so the + * pipes never fill and block it, then unref both the stdio sockets and the child so it + * never keeps the event loop alive on its own. `resume()` keeps the pipe sockets + * referenced, so unref-ing the child alone leaves a one-shot host (a CLI/batch/test + * process that calls `ai()` once) unable to exit — the sockets must be unref'd too. On a + * later exit/error, forget the memoized handle so the next call respawns rather than + * reusing a dead URL (`trackChild` already installed the reaper and stream guards at + * spawn time). + */ +function keepChildAlive(child: ChildProcess, onGatewayLost: () => void): void { + const onGone = () => { + if (liveChild === child) liveChild = undefined + onGatewayLost() + } + child.on("error", onGone) + child.on("exit", onGone) + child.stdout?.resume() + child.stderr?.resume() + unrefStream(child.stdout) + unrefStream(child.stderr) + child.unref() +} diff --git a/packages/ai-gateway/src/index.ts b/packages/ai-gateway/src/index.ts new file mode 100644 index 000000000..8db0ea21c --- /dev/null +++ b/packages/ai-gateway/src/index.ts @@ -0,0 +1,48 @@ +/** + * `@alienplatform/ai-gateway`: the thin TypeScript wrapper that starts the Alien + * AI gateway as a local subprocess. + * + * The gateway itself is a single Rust implementation (the `alien-ai-gateway` + * binary). This package spawns that binary once per process (or reuses the URL a + * container launcher already exported) and returns its 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. + * + * @example + * ```typescript + * import { getAiConnection } from "@alienplatform/ai-gateway" + * import { createOpenAICompatible } from "@ai-sdk/openai-compatible" + * const provider = createOpenAICompatible({ name: "alien", ...(await getAiConnection("assistant")) }) + * ``` + */ + +import { createAiClient } from "./client.js" +import { createGateway } from "./gateway.js" +import { resolveGatewayBinary } from "./loader.js" + +const gateway = createGateway(resolveGatewayBinary) +const client = createAiClient(gateway) + +/** Start the AI gateway subprocess (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, + 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..6c96666d9 --- /dev/null +++ b/packages/ai-gateway/src/loader.ts @@ -0,0 +1,210 @@ +/** + * Locate (and, for a compiled Worker, extract) the `alien-ai-gateway` executable + * that the gateway wrapper spawns. + * + * Resolution order (deferred to first spawn, so importing the package does no I/O): + * + * 1. An embedded binary registered by the `./native` entry. A `bun build + * --compile` Worker embeds the executable and hands us its (virtual) path; + * we copy it to a real, executable temp file once. + * 2. `ALIEN_AI_GATEWAY_BINARY_PATH`: an explicit path (dev/test escape hatch; + * never set in published installs). + * 3. The per-platform prebuild package `@alienplatform/ai-gateway-`, + * which ships the executable (installed via `optionalDependencies`). + * 4. Dev fallback: the locally-built binary under `target/{release,debug}`, + * found by walking up from this module. + */ + +import { chmodSync, existsSync, mkdtempSync, writeFileSync } from "node:fs" +import { createRequire } from "node:module" +import { tmpdir } from "node:os" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" +import { AlienError } from "@alienplatform/core" + +import { GatewayBinaryUnavailableError, UnsupportedPlatformError } from "./errors.js" + +const require = createRequire(import.meta.url) + +/** The launcher binary's file name across all platforms. */ +const BINARY_NAME = "alien-ai-gateway" + +/** A running gateway handle: its loopback base URL. */ +export interface RawAiGatewayHandle { + readonly url: 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 prebuild triple. The binary is built for + * musl as well as glibc, so Alpine/musl images resolve a prebuild rather than + * being rejected for their libc. + */ +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") { + if (arch === "x64") return `linux-x64-${libc}` + if (arch === "arm64") return `linux-arm64-${libc}` + } + throw new AlienError(UnsupportedPlatformError.create({ platform, arch })) +} + +/** Walk up from `startDir` to find the locally-built binary, or `undefined`. */ +export function findLocalBinary( + startDir: string = dirname(fileURLToPath(import.meta.url)), +): string | undefined { + let dir = startDir + for (;;) { + for (const profile of ["release", "debug"]) { + const candidate = join(dir, "target", profile, BINARY_NAME) + if (existsSync(candidate)) return candidate + } + const parent = dirname(dir) + if (parent === dir) return undefined + dir = parent + } +} + +let cached: string | undefined +let embeddedBinaryPath: string | undefined + +/** + * Register the bun-embedded binary path, so plain `@alienplatform/ai-gateway` + * imports (which resolve through {@link resolveGatewayBinary}) find it inside a + * `bun build --compile` binary, where the filesystem/prebuild resolution below + * cannot. In a normal install this is never called. The `/native` entry calls it. + */ +export function registerEmbeddedBinary(path: string): void { + embeddedBinaryPath = path +} + +/** Resolve (and memoize) a runnable path to the launcher binary, or throw. */ +export async function resolveGatewayBinary(): Promise { + if (cached) return cached + + // A compiled binary registers its embedded copy up front; extract it to a real, + // executable file, since the embedded path is virtual and cannot be spawned. + if (embeddedBinaryPath) { + cached = await extractEmbedded(embeddedBinaryPath) + return cached + } + + const override = process.env.ALIEN_AI_GATEWAY_BINARY_PATH + if (override) { + // Resolved against cwd, not this module's dist/ directory. + cached = ensureExecutable(resolve(override)) + return cached + } + + const triple = platformTriple() + const prebuild = await resolvePrebuild(triple) + if (prebuild) { + cached = ensureExecutable(prebuild) + return cached + } + + const local = findLocalBinary() + if (local) { + cached = ensureExecutable(local) + return cached + } + + throw new AlienError( + GatewayBinaryUnavailableError.create({ + triple, + reason: `no embedded binary, no ALIEN_AI_GATEWAY_BINARY_PATH, no '@alienplatform/ai-gateway-${triple}' prebuild, and no locally-built target/{release,debug}/${BINARY_NAME}; build it with \`cargo build --bin ${BINARY_NAME} -p alien-ai-gateway\``, + }), + ) +} + +/** + * Copy a bun-embedded binary to a real, executable temp file. Only reached inside + * a `bun build --compile` binary, where `Bun` is present and the embedded file + * must live on disk with the execute bit set before it can be spawned. + */ +async function extractEmbedded(virtualPath: string): Promise { + const bun = (globalThis as { Bun?: { file(p: string): { arrayBuffer(): Promise } } }) + .Bun + if (!bun) { + throw new AlienError( + GatewayBinaryUnavailableError.create({ + triple: platformTriple(), + path: virtualPath, + reason: "an embedded gateway binary can only be extracted under the Bun runtime", + }), + ) + } + try { + const bytes = await bun.file(virtualPath).arrayBuffer() + const dir = mkdtempSync(join(tmpdir(), "alien-ai-gateway-")) + const exe = join(dir, BINARY_NAME) + writeFileSync(exe, Buffer.from(bytes)) + chmodSync(exe, 0o755) + return exe + } catch (error) { + // A filesystem fault here (no temp space, read-only tmp) would otherwise + // surface as a bare Error; wrap it so callers still see an AlienError. + throw (await AlienError.from(error)).withContext( + GatewayBinaryUnavailableError.create({ + triple: platformTriple(), + path: virtualPath, + reason: "failed to extract the embedded gateway binary", + }), + ) + } +} + +/** Best-effort execute bit; a prebuild may already be executable or read-only. */ +function ensureExecutable(path: string): string { + try { + chmodSync(path, 0o755) + } catch { + // A read-only mount can't be chmod'd; if it isn't already executable the + // spawn below surfaces a real, specific error. + } + return path +} + +/** Resolve the binary shipped by the per-platform prebuild package, or `undefined`. */ +async function resolvePrebuild(triple: string): Promise { + const pkg = `@alienplatform/ai-gateway-${triple}` + let pkgJson: string + try { + pkgJson = require.resolve(`${pkg}/package.json`) + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") return undefined + // Installed but unresolvable (e.g. a corrupt manifest); preserve the cause. + throw (await AlienError.from(error)).withContext( + GatewayBinaryUnavailableError.create({ + triple, + reason: `the '${pkg}' prebuild is installed but could not be resolved`, + }), + ) + } + const candidate = join(dirname(pkgJson), BINARY_NAME) + return existsSync(candidate) ? candidate : undefined +} + +/** Test-only: reset the memoized resolution. */ +export function resetGatewayBinaryCacheForTests(): void { + cached = undefined + embeddedBinaryPath = undefined +} diff --git a/packages/ai-gateway/src/native.ts b/packages/ai-gateway/src/native.ts new file mode 100644 index 000000000..f56ccb00f --- /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 `alien-ai-gateway` executable through a literal `with { type: "file" }` + * specifier so bun embeds it into the single-file binary, and registers its path so + * the default loader extracts and spawns it. Unlike the default entry it does not + * probe the filesystem: the binary 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 embeddedBinaryPath from "./alien-ai-gateway.bin" with { type: "file" } +import { createAiClient } from "./client.js" +import { createGateway } from "./gateway.js" +import { registerEmbeddedBinary, resolveGatewayBinary } from "./loader.js" + +/** + * Register the bun-embedded gateway binary with the default loader, so plain + * `@alienplatform/ai-gateway` imports — including the SDK's re-exported `ai()`, + * which resolves through {@link resolveGatewayBinary}, to spawn 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 installEmbeddedGateway(): void { + registerEmbeddedBinary(embeddedBinaryPath) +} + +const gateway = createGateway(resolveGatewayBinary) +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..b7296a400 --- /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 embedded binary: the `./alien-ai-gateway.bin` specifier in + // native.ts must survive into the output as a literal so bun can embed it. + external: [/\.bin$/], +}) 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..acc67c39f --- /dev/null +++ b/packages/core/src/__tests__/ai.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest" +import { AI } from "../ai.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") + }) +}) diff --git a/packages/core/src/ai.ts b/packages/core/src/ai.ts new file mode 100644 index 000000000..c805ac380 --- /dev/null +++ b/packages/core/src/ai.ts @@ -0,0 +1,44 @@ +import { type Ai as AiConfig, AiSchema, type ResourceType } from "./generated/index.js" +import { Resource } from "./resource.js" + +export type { AiOutputs, Ai as AiConfig } from "./generated/index.js" +export { AiSchema as AiConfigSchema } from "./generated/index.js" + +/** + * 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 + } + + /** + * 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..ad0ab8b5d 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,6 +161,7 @@ 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 { GcpArtifactRegistryHeartbeatData } from "./zod/gcp-artifact-registry-heartbeat-data-schema.js"; export type { GcpArtifactRegistryImportData } from "./zod/gcp-artifact-registry-import-data-schema.js"; @@ -185,6 +192,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 +379,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 +392,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 +446,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,6 +541,7 @@ 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 { GcpArtifactRegistryHeartbeatDataSchema } from "./zod/gcp-artifact-registry-heartbeat-data-schema.js"; export { GcpArtifactRegistryImportDataSchema } from "./zod/gcp-artifact-registry-import-data-schema.js"; @@ -557,6 +572,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..713b9a9ff --- /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.","required":["id"],"properties":{"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/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..c16df54de --- /dev/null +++ b/packages/core/src/generated/zod/ai-schema.ts @@ -0,0 +1,15 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import * as z from "zod"; + +/** + * @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. + */ +export const AiSchema = z.object({ + "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.") + +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/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..abfaf822c 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,6 +161,7 @@ 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 { GcpArtifactRegistryHeartbeatData } from "./gcp-artifact-registry-heartbeat-data-schema.js"; export type { GcpArtifactRegistryImportData } from "./gcp-artifact-registry-import-data-schema.js"; @@ -185,6 +192,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 +379,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 +392,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 +446,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,6 +541,7 @@ 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 { GcpArtifactRegistryHeartbeatDataSchema } from "./gcp-artifact-registry-heartbeat-data-schema.js"; export { GcpArtifactRegistryImportDataSchema } from "./gcp-artifact-registry-import-data-schema.js"; @@ -557,6 +572,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 ff221963b..1df79df70 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,6 +12,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..d52a87a86 100644 --- a/packages/package-layout/fixture/src/compile-entry.ts +++ b/packages/package-layout/fixture/src/compile-entry.ts @@ -1,26 +1,59 @@ /** * 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 embedded native pieces: + * the `@alienplatform/bindings` napi addon (kv/storage/queue/vault, in-process) + * and the `@alienplatform/ai-gateway` launcher binary (the gateway runs as a + * spawned process). The SDK's re-exported factories then resolve those with no + * filesystem lookup, 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 bindings `.node` and the `alien-ai-gateway.bin` binary are only staged by + * the release pipeline (.github/workflows/release.yml) or by `run.ts` here, so in + * a workspace checkout with no staged assets the `bun build --compile` step fails. */ import { storage } from "@alienplatform/bindings/native" +import { ai, startAiGateway } from "@alienplatform/sdk" +import { installEmbeddedAddon } from "@alienplatform/sdk/native" + +// Register both embedded pieces up front, exactly as a compiled Worker bootstrap +// does: this eagerly wires the bindings addon and the ai-gateway binary path. +installEmbeddedAddon() + +// Reference the factories so the compiler must stage the assets: `storage` via +// the direct bindings/native path, `ai` via the SDK re-export. +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`) + } +} -// 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") +// The heart of the addon-to-binary switch: prove the embedded gateway binary +// extracts, spawns, and reports a loopback URL, from inside the compiled +// single-file binary, with no gateway on disk (it came from the embed). Wrapped +// in an async IIFE because `--format=cjs` (required for the embed) has no +// top-level await. +async function main(): Promise { + const handle = await startAiGateway() + if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(handle.url)) { + throw new Error(`expected a loopback gateway URL from the embedded binary, got: ${handle.url}`) + } + console.log( + `compile-entry: bindings addon embedded; ai-gateway binary embedded, extracted, and spawned (${handle.url})`, + ) + // The spawned gateway is unref'd; exit deterministically so the smoke never hangs. + process.exit(0) } -console.log("compile-entry: native binding factory embedded") +main().catch(error => { + console.error(error) + process.exit(1) +}) diff --git a/packages/package-layout/steps/compile.ts b/packages/package-layout/steps/compile.ts index af15a9ea9..a573e4880 100644 --- a/packages/package-layout/steps/compile.ts +++ b/packages/package-layout/steps/compile.ts @@ -1,111 +1,122 @@ -// 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 native +// asset through a literal specifier (`./alien-bindings.node`, the bindings addon; +// `./alien-ai-gateway.bin`, the gateway launcher binary) 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, aiBinaryPath } = 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.bin", + ), + addonPath: aiBinaryPath, + }, + ] 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 native 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..c02c660d5 100644 --- a/packages/package-layout/steps/ensure-addon.ts +++ b/packages/package-layout/steps/ensure-addon.ts @@ -1,88 +1,147 @@ -// Step 3.5 — ensure a native addon exists for the host platform. +// Step 3.5: ensure the host native pieces exist for the compile-smoke. // -// 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. +// Two shapes: `@alienplatform/bindings` ships a napi addon (`.node`), and +// `@alienplatform/ai-gateway` ships a standalone launcher binary. Each resolves +// in order: an override / the per-platform prebuild (optionalDependencies, only +// injected at publish time) / a locally-built artifact. CI has neither prebuild +// nor a dev artifact, so build one ourselves and hand its path to the compile +// step for staging. Skipped (and logged) whenever an artifact 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). import { platformTriple } from "../../bindings/src/loader.ts" import { type CheckResult, type Ctx, lastLine, run } from "./shared.ts" +const GATEWAY_BINARY = "alien-ai-gateway" +const GATEWAY_CRATE = "alien-ai-gateway" + +/** 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.`, + /** + * Resolve (or build) the bindings 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(devAddonPath)) { - ctx.addonPath = devAddonPath - console.log( - `[addon] using existing dev addon at ${relative(scriptDir, devAddonPath)} (fast path, no build).`, - ) - } 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.", + 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 + } + + /** + * Resolve (or cargo-build) the host `alien-ai-gateway` launcher binary that the + * ai-gateway package spawns. Returns its path, or `undefined` when a prebuild + * is installed (resolves via node_modules) or the build fails. + */ + function resolveGatewayBinary(): string | undefined { + const prebuildInstalledDir = join( + fixtureDir, + "node_modules", + "@alienplatform", + `ai-gateway-${triple}`, + ) + if (existsSync(prebuildInstalledDir)) { + console.log( + `[gateway] per-platform ai-gateway binary prebuild installed for '${triple}'; no build needed.`, ) - 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 undefined + } + for (const profile of ["release", "debug"]) { + const candidate = join(repoRoot, "target", profile, GATEWAY_BINARY) + if (existsSync(candidate)) { + console.log( + `[gateway] using existing ${profile} binary at ${relative(scriptDir, candidate)} (fast path, no build).`, + ) + return candidate + } } + console.log( + `[gateway] no prebuild and no built binary; building with \`cargo build --release --bin ${GATEWAY_BINARY} -p ${GATEWAY_CRATE}\` (CI path)...`, + ) + const build = run( + "cargo", + ["build", "--release", "--bin", GATEWAY_BINARY, "-p", GATEWAY_CRATE], + repoRoot, + ) + const built = join(repoRoot, "target", "release", GATEWAY_BINARY) + if (build.status === 0 && existsSync(built)) { + console.log(`[gateway] built ${relative(scriptDir, built)}.`) + return built + } + console.error( + `[gateway] cargo build of ${GATEWAY_BINARY} failed; the compile check below will fail to embed the gateway.`, + ) + results.push({ + check: "addon-build", + package: "ai-gateway", + status: "fail", + reason: `cargo build --release --bin ${GATEWAY_BINARY} -p ${GATEWAY_CRATE} did not produce a binary on this host`, + evidence: lastLine(build.stderr) || lastLine(build.stdout) || `exit ${build.status}`, + }) + return undefined } + ctx.addonPath = resolveAddon("bindings", "alien-bindings-node") + ctx.aiBinaryPath = resolveGatewayBinary() 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..0e88ff791 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 launcher-binary path, when one is available for this host. */ + aiBinaryPath?: 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..a7db6b988 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -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..dcbde4d88 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -58,9 +58,32 @@ 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 (a spawned Rust gateway process) +// ============================================================================ + +export { + ai, + getAiConnection, + startAiGateway, + Ai, + isExternalAiBinding, + parseAiBinding, +} from "@alienplatform/ai-gateway" +export type { + AiBinding, + AmbientAiBinding, + ExternalAiBinding, + AiConnection, + AiModel, + ChatCompletionCreateParams, + 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..09a3e89c0 100644 --- a/packages/sdk/src/native.ts +++ b/packages/sdk/src/native.ts @@ -1,26 +1,31 @@ /** - * `@alienplatform/sdk/native` — the embedded-addon bridge for compiled Workers. + * `@alienplatform/sdk/native`: the embedded-native 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 + * pieces 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`) + * and embeds each package's native asset: the `alien-bindings.node` addon + * (bindings runs in-process) and the `alien-ai-gateway` executable (the gateway + * runs as a spawned process). The SDK keeps both packages external (see + * tsdown.config.ts), so the registrations here are visible to the SDK's + * re-exported `kv`/`storage`/`queue`/`vault` and `ai`/`getAiConnection` 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. + * Imported only by a generated compiled entry (never in dev), so the transitive + * static asset imports inside each `/native` only run where the asset was staged. */ -export { installEmbeddedAddon } from "@alienplatform/bindings/native" +import { installEmbeddedGateway } from "@alienplatform/ai-gateway/native" +import { installEmbeddedAddon as installBindingsAddon } from "@alienplatform/bindings/native" + +/** Register the bun-embedded bindings addon and ai-gateway binary with their loaders. */ +export function installEmbeddedAddon(): void { + installBindingsAddon() + installEmbeddedGateway() +} 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..c2ae88b46 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 + '@types/node': + specifier: ^24.0.15 + version: 24.10.4 + openai: + specifier: ^4 + version: 4.104.0(ws@8.18.0)(zod@4.3.2) + 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 @@ -2130,9 +2164,15 @@ packages: '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.19.13': resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==} @@ -2265,6 +2305,10 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -2798,10 +2842,21 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@2.5.5: resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} engines: {node: '>= 0.12'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2912,6 +2967,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} @@ -2945,6 +3004,9 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -3341,6 +3403,11 @@ packages: nice-grpc@2.1.14: resolution: {integrity: sha512-GK9pKNxlvnU5FAdaw7i2FFuR9CqBspcE+if2tqnKXBcE0R8525wj4BZvfcwj7FjvqbssqKxRHt2nwedalbJlww==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch-h2@2.3.0: resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==} engines: {node: 4.x || >=6.0.0} @@ -3396,6 +3463,18 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -3964,6 +4043,9 @@ packages: unconfig@7.4.2: resolution: {integrity: sha512-nrMlWRQ1xdTjSnSUqvYqJzbTBFugoqHobQj58B2bc8qxHKBBHMNNsWQFP3Cd3/JZK907voM2geYPWqD4VK3MPQ==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -4088,6 +4170,10 @@ packages: jsdom: optional: true + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -6336,8 +6422,17 @@ snapshots: '@types/long@4.0.2': {} + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 24.10.4 + form-data: 4.0.6 + '@types/node@12.20.55': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@22.19.13': dependencies: undici-types: 6.21.0 @@ -6520,6 +6615,10 @@ snapshots: agent-base@7.1.4: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -6819,7 +6918,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 es-toolkit@1.49.0: {} @@ -7048,6 +7147,8 @@ snapshots: flatted@3.3.3: {} + form-data-encoder@1.7.2: {} + form-data@2.5.5: dependencies: asynckit: 0.4.0 @@ -7057,6 +7158,19 @@ snapshots: mime-types: 2.1.35 safe-buffer: 5.2.1 + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -7108,7 +7222,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-port@7.1.0: {} @@ -7202,6 +7316,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + headers-polyfill@4.0.3: optional: true @@ -7242,6 +7360,10 @@ snapshots: human-id@4.1.3: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -7593,6 +7715,8 @@ snapshots: abort-controller-x: 0.4.3 nice-grpc-common: 2.0.2 + node-domexception@1.0.0: {} + node-fetch-h2@2.3.0: dependencies: http2-client: 1.3.5 @@ -7674,6 +7798,21 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + openai@4.104.0(ws@8.18.0)(zod@4.3.2): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.18.0 + zod: 4.3.2 + transitivePeerDependencies: + - encoding + openapi-types@12.1.3: {} optionator@0.9.4: @@ -8249,6 +8388,8 @@ snapshots: quansync: 1.0.0 unconfig-core: 7.4.2 + undici-types@5.26.5: {} + undici-types@6.21.0: {} undici-types@7.16.0: {} @@ -8364,6 +8505,8 @@ snapshots: - tsx - yaml + web-streams-polyfill@4.0.0-beta.3: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: diff --git a/tests/e2e/test-apps/comprehensive-typescript/alien.ts b/tests/e2e/test-apps/comprehensive-typescript/alien.ts index d0f7a1098..0aa249b8a 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..db1617ddc --- /dev/null +++ b/tests/e2e/test-apps/comprehensive-typescript/src/handlers/ai.ts @@ -0,0 +1,84 @@ +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 ENABLED models (getAvailableModels is availability-filtered) and invokes +// every one through the gateway: the full app -> gateway -> cloud LLM path under the +// workload's ambient credentials. Invoking each listed model is only sound because +// the list is filtered to what is actually enabled; a 403/404 on a listed model +// would mean the availability filter is broken. +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() + if (models.length === 0) { + return c.json( + { injected: true, service: config.service, locator, error: "no models available" }, + 500, + ) + } + // Invoke every listed model with a one-token probe. `ok` is true on a real + // completion or a 429 rate-limit (the model is enabled, the account is + // quota-limited); anything else on a listed model is a filter bug. + const results = await Promise.all( + models.map(async model => { + try { + await binding.chat.completions.create({ + model: model.id, + max_completion_tokens: 1, + messages: [{ role: "user", content: "ping" }], + }) + return { model: model.id, ok: true } + } catch (error) { + // Classify on the live error instance: toExternal() sanitizes internal + // errors down to a generic message, which would hide the 429 status. + // toOptions() keeps the real detail for this same-process diagnostic. + const ok = error instanceof AlienError && error.httpStatusCode === 429 + const detail = + error instanceof AlienError ? JSON.stringify(error.toOptions()) : String(error) + return { model: model.id, ok, detail } + } + }), + ) + return c.json({ + injected: true, + service: config.service, + locator, + modelCount: models.length, + models: models.map(m => ({ id: m.id, provider: m.provider, displayName: m.displayName })), + results, + }) + } 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)