diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 131e1b2dc5..0820181028 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -1,5 +1,19 @@
# Compatibility Contract
+## Server v1.6.560 source candidate
+
+Not yet published. Install examples remain on verified published 559.
+The candidate consumes published Web Console 1.6.201 / optional bridge 0.2.3
+and Engine 0.183.360 original CI artifacts. It adds External Service, DNS Alias
+and metadata-only audit flows, corrects the Catalog-validator collection
+admission and retains audit events across a rejected typed-write transaction.
+Native SQL/Key/role checks and unknown-filter denials are preserved.
+Catalog 0.20.17 / Compose 0.14.40 and official Brotli package proof are unchanged.
+Combined runtime, role, cross-ID, layout, weak-model and durable-DENY acceptance
+remain pending. This does not complete the 243-group frontend inventory.
+See the [candidate note](docs/releases/server-1.6.560.md) for identities and scope.
+
+
## Server v1.6.559 published
diff --git a/docs/releases/server-1.6.560.md b/docs/releases/server-1.6.560.md
new file mode 100644
index 0000000000..b2e862d104
--- /dev/null
+++ b/docs/releases/server-1.6.560.md
@@ -0,0 +1,69 @@
+# Server v1.6.560 candidate
+
+Not published or runtime accepted. Install examples stay on published 559.
+Both components passed required CI, same-tree merge and immutable publication.
+All original public asset bytes were read back anonymously. Server assembly
+and designated live acceptance remain separate pending gates.
+
+## Behavior and owner boundaries
+
+The matching Web Console 1.6.201 / bridge 0.2.3 and Engine 0.183.360 add bounded
+External Service, DNS Alias and audit metadata workflows. External Service
+creation is stopped; hostname and IP modes are mutually exclusive. DNS Alias
+creation, local target binding and activation are separate reviewed changes.
+Advanced links and incoming-consumer cleanup remain unavailable.
+
+Fix project-scoped native and typed collection admission through the final
+Catalog response validator. The actual SQL authorization guard attests only
+this known query-preserving validator; unknown/synthetic filters and altered
+query/pagination implementations still fail closed. RBAC and Key policy are
+unchanged, including explicit denials and cross-project/parent isolation.
+
+Audit queries use the installed broker's existing authorized sources via a
+fixed bounded CLI and private stdin. Public tools receive safe metadata, never
+cookies, Key secrets, arbitrary URLs/SQL or raw audit payloads. Engine records
+the owner's actual query result; payload inspection/export remain human-only.
+Catalog/Archive behavior and four independent default-off opt-ins are retained.
+
+Audit events created in a Core write transaction retain their forced durable
+spool until the existing independent replayer can commit delivery. A rejected
+typed read must remain auditable after its outer write rolls back. The fix
+keeps current metadata, capacity and failure behavior without nested account
+locks. Source/transaction tests passed; designated live durability is pending.
+
+## Required acceptance: pending
+
+The previous 558 Service-stop failure, 559 post-create collection failure and
+their original reports remain recorded. No partial result is promoted to a
+whole workflow PASS. The designated 8080 complete Core lifecycle, Catalog
+launch/Archive, new domains, affected Key/role/cross-ID, layouts and weak-model
+cases remain pending. Retain only unaffected immutable evidence.
+
+The baseline remains 243 business groups and 3,089 frontend inventory rows;
+this candidate does not complete that inventory. Browser SBOM coverage retains
+its recorded limits. KVM HOLD and explicit human ceremonies are not API grants.
+
+## Upgrade and rollback
+
+Deploy only the new immutable published image after necessary checks. Preserve
+existing persistent volumes, origin/session/MFA controls, operator/JVM settings,
+restart policy and AppArmor. Take normal database/volume backups and retain the
+previous container/image. Never replay uncertain writes on upgrade/rollback;
+settle or cancel plans and revoke temporary Keys. A matching database backup
+may be required for rollback. Do not delete volumes or plan receipts.
+
+Production, HAProxy, firewall and production data are outside this QA change.
+Official Brotli package proof and all other package gates remain exact. No VEX
+scope expansion or weakened security threshold is introduced.
+
+## Qualified component coordinates
+
+- Web 1.6.201 / bridge 0.2.3 producer: `24715414f143e2c58cf53084bd84cbea0922798f`; tag source `18acf40d85061533762e8d0d1b9c1f7be69afe9c`.
+- Web archive SHA256: `5bc8142d9322657c5cbae782756c3aa5a45b6537074de8749066492fa8947eb8`.
+- Runtime SBOM SHA256: `8b837eb330b9815df48b4256d3bf0c4e215864b996e2be40dd707ec532872503`; lock `d255b74a835ab7c0224d12613baacdf021d8b30d46204a71c22d57f35725666f`.
+- Bridge archive SHA256: `aad5cadfd3806b0f2c7339a1585a6bd19f3e61585af91dd4da119514775e7e93`; 13 published assets verified.
+- Engine v0.183.360 producer: `72230c8b471312377bae968c0c17d63e8ad97ffb`; tag source `d6845eedd1df5ae8e69cc075d80b166bc8d26731`.
+- Engine JAR SHA256: `0ebbd517ee43e113f29cbc14fe542aa7c7900ae28d65d63a12c348b7f0ac9a21`; runtime SBOM `088ce2b94ea1c30a6d1e52e600d2ef45258aeb26cdded784407a91020a19f474`.
+- Original Engine CI: 408 suites / 3822 tests / zero failure or error / one existing skip; 12 published assets verified.
+- Engine provenance is original GitHub artifact/source/tree evidence; its workflow supplies no native SLSA attestation.
+- Catalog 0.20.17 and Compose 0.14.40 retain the qualified original pins from Server559; other component/package inputs are unchanged.
diff --git a/scripts/check-server-api-explorer-patch.sh b/scripts/check-server-api-explorer-patch.sh
index 0ed1ad823e..09881409c2 100755
--- a/scripts/check-server-api-explorer-patch.sh
+++ b/scripts/check-server-api-explorer-patch.sh
@@ -533,7 +533,7 @@ require_marker "$release_dockerfile" \
SERVER_INCREMENTAL_RELEASE_BASE_MISSING
for web_sbom_release_marker in \
'COPY --chmod=0755 artifacts/verify-web-console-sbom.py /usr/local/bin/verify-web-console-sbom' \
- 'ARG WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT=web-console-1.6.200-runtime.cdx.json' \
+ 'ARG WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT=web-console-1.6.201-runtime.cdx.json' \
'"${WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT}" "${WEB_CONSOLE_RUNTIME_SBOM_SHA256}" "${web_sbom}";' \
'--lock-sha256 "${WEB_CONSOLE_LOCK_SHA256}" --archive "${web_archive}"' \
'--webmcp-ceiling enabled;' \
@@ -571,7 +571,7 @@ python3 scripts/test-web-console-sbom.py
python3 server/security/test_web_console_sbom_scan.py
require_marker "$release_dockerfile" \
- 'ARG SERVER_RELEASE_TAG=v1.6.559' \
+ 'ARG SERVER_RELEASE_TAG=v1.6.560' \
SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING
require_marker "$release_dockerfile" \
'org.opencontainers.image.version="${SERVER_RELEASE_TAG}"' \
@@ -629,22 +629,22 @@ require_marker "$publish_workflow" \
'bash source/server/artifacts/verify-host-api-package.sh' \
SERVER_HOST_API_PRODUCER_RELEASE_CHECK_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.200' \
+ 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.201' \
SERVER_INCREMENTAL_WEB_CONSOLE_VERSION_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.200.tar.gz' \
+ 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.201.tar.gz' \
SERVER_INCREMENTAL_WEB_CONSOLE_ARTIFACT_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_ARTIFACT_SHA256=97fe46c426cb83263fcfc7f5884c15ca90f7ec3ebec71d9ca023d8b7ad2665c0' \
+ 'ARG WEB_CONSOLE_ARTIFACT_SHA256=5bc8142d9322657c5cbae782756c3aa5a45b6537074de8749066492fa8947eb8' \
SERVER_INCREMENTAL_WEB_CONSOLE_HASH_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_COMMIT=03e0d7b25b8dfcbc45baecf6d448834016aac649' \
+ 'ARG WEB_CONSOLE_COMMIT=24715414f143e2c58cf53084bd84cbea0922798f' \
SERVER_INCREMENTAL_WEB_CONSOLE_COMMIT_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_LOCK_SHA256=1797dd27bb7b1c6b86b8689435e916b03523a9fdf45c5178ec7c2ea05706d4bd' \
+ 'ARG WEB_CONSOLE_LOCK_SHA256=d255b74a835ab7c0224d12613baacdf021d8b30d46204a71c22d57f35725666f' \
SERVER_INCREMENTAL_WEB_CONSOLE_LOCK_HASH_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_RUNTIME_SBOM_SHA256=0ce1f6b99e1e449c545d3d7df65302fdb58d1974fecea6477b20262c2148921c' \
+ 'ARG WEB_CONSOLE_RUNTIME_SBOM_SHA256=8b837eb330b9815df48b4256d3bf0c4e215864b996e2be40dd707ec532872503' \
SERVER_INCREMENTAL_WEB_CONSOLE_SBOM_HASH_MISSING
require_marker "$release_dockerfile" \
"grep -aF 'hostsPage.permissionDenied'" \
@@ -653,22 +653,22 @@ require_marker "$release_dockerfile" \
'"hostsPage.permissionDenied":"您沒有權限在此環境中新增主機。"' \
SERVER_INCREMENTAL_WEB_CONSOLE_ZH_TW_PERMISSION_MESSAGE_MISSING
require_marker "$build_script" \
- 'web_console_commit=${WEB_CONSOLE_COMMIT:-03e0d7b25b8dfcbc45baecf6d448834016aac649}' \
+ 'web_console_commit=${WEB_CONSOLE_COMMIT:-24715414f143e2c58cf53084bd84cbea0922798f}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_COMMIT_MISSING
require_marker "$build_script" \
- 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.200}' \
+ 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.201}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_VERSION_MISSING
require_marker "$build_script" \
- 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.200.tar.gz}' \
+ 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.201.tar.gz}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_ARTIFACT_MISSING
require_marker "$build_script" \
- 'web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-97fe46c426cb83263fcfc7f5884c15ca90f7ec3ebec71d9ca023d8b7ad2665c0}' \
+ 'web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-5bc8142d9322657c5cbae782756c3aa5a45b6537074de8749066492fa8947eb8}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_HASH_MISSING
require_marker "$build_script" \
- 'web_console_lock_sha256=${WEB_CONSOLE_LOCK_SHA256:-1797dd27bb7b1c6b86b8689435e916b03523a9fdf45c5178ec7c2ea05706d4bd}' \
+ 'web_console_lock_sha256=${WEB_CONSOLE_LOCK_SHA256:-d255b74a835ab7c0224d12613baacdf021d8b30d46204a71c22d57f35725666f}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_LOCK_HASH_MISSING
require_marker "$build_script" \
- 'web_console_runtime_sbom_sha256=${WEB_CONSOLE_RUNTIME_SBOM_SHA256:-0ce1f6b99e1e449c545d3d7df65302fdb58d1974fecea6477b20262c2148921c}' \
+ 'web_console_runtime_sbom_sha256=${WEB_CONSOLE_RUNTIME_SBOM_SHA256:-8b837eb330b9815df48b4256d3bf0c4e215864b996e2be40dd707ec532872503}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_SBOM_HASH_MISSING
require_marker "$release_dockerfile" \
'ARG CATALOG_SERVICE_VERSION=0.20.17' \
@@ -1139,21 +1139,21 @@ require_marker "$build_script" \
'--file server/Dockerfile.web-compose-release' \
SERVER_INCREMENTAL_RELEASE_BUILD_PATH_MISSING
require_marker "$build_script" \
- 'image=${IMAGE:-pasturestack-validation/server:v1.6.559}' \
+ 'image=${IMAGE:-pasturestack-validation/server:v1.6.560}' \
SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING
require_marker "$build_script" \
- 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.559}' \
+ 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.560}' \
SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING
require_marker "$build_script" \
'CATTLE_RANCHER_SERVER_VERSION="${server_release_tag}"' \
SERVER_INCREMENTAL_RELEASE_BUILD_RUNTIME_VERSION_MISSING
for release_engine_marker in \
- 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.359' \
+ 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.360' \
'ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar' \
- 'ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=07e7de2548bd98d5209bf3594d69fe5b9a5d7ddba36eed4b6b6e4406022c3f90' \
- 'ARG ORCHESTRATION_ENGINE_COMMIT=eac3fde5fc7779b6107e2702785120a9c591927b' \
+ 'ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=0ebbd517ee43e113f29cbc14fe542aa7c7900ae28d65d63a12c348b7f0ac9a21' \
+ 'ARG ORCHESTRATION_ENGINE_COMMIT=72230c8b471312377bae968c0c17d63e8ad97ffb' \
'COPY --from=release_artifacts /out/orchestration-engine.jar /tmp/orchestration-engine.jar' \
- 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.359' \
+ 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.360' \
'sh /usr/share/cattle/verify-engine-archive.sh "${archive}" "${ORCHESTRATION_ENGINE_VERSION}" "${ORCHESTRATION_ENGINE_ARTIFACT_SHA256}"' \
'cattle-resources-${ORCHESTRATION_ENGINE_VERSION}.jar' \
'cattle-app-config-${ORCHESTRATION_ENGINE_VERSION}.jar' \
@@ -1172,10 +1172,10 @@ for release_engine_marker in \
SERVER_INCREMENTAL_ENGINE_REPLACEMENT_MISSING
done
for release_engine_build_marker in \
- 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.359}' \
+ 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.360}' \
'orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar}' \
- 'orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-07e7de2548bd98d5209bf3594d69fe5b9a5d7ddba36eed4b6b6e4406022c3f90}' \
- 'orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-eac3fde5fc7779b6107e2702785120a9c591927b}' \
+ 'orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-0ebbd517ee43e113f29cbc14fe542aa7c7900ae28d65d63a12c348b7f0ac9a21}' \
+ 'orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-72230c8b471312377bae968c0c17d63e8ad97ffb}' \
'CATTLE_CATTLE_VERSION="${orchestration_engine_release_tag}"' \
'cattle-resources-${ORCHESTRATION_ENGINE_VERSION}.jar' \
'< "${server_dir}/artifacts/verify-engine-archive.sh"'; do
@@ -1694,10 +1694,10 @@ fi
if grep -Fxq '## Current source candidate' README.md; then
for candidate_readme_marker in \
'' \
- 'Server `v1.6.559` is a prepared source candidate, not a published image.' \
+ 'Server `v1.6.560` is a prepared source candidate, not a published image.' \
'run them only after its official release' \
'This candidate is not yet available' \
- 'ghcr.io/pasturestack/server:v1.6.559'; do
+ 'ghcr.io/pasturestack/server:v1.6.560'; do
require_marker README.md "$candidate_readme_marker" \
SERVER_CURRENT_CANDIDATE_README_MISSING
done
@@ -2303,7 +2303,7 @@ done
jq -e '
["CVE-2026-107928","CVE-2026-107929","CVE-2026-86409","CVE-2026-86410","CVE-2026-86412","CVE-2026-86414","CVE-2026-87862","CVE-2026-87863","CVE-2026-87864","CVE-2026-87865","CVE-2026-92386","CVE-2026-92387","CVE-2026-92388","CVE-2026-92389","CVE-2026-92390","CVE-2026-92391","CVE-2026-92392","CVE-2026-94266"] as $curl_fixed_cves
| .["@context"] == "https://openvex.dev/ns/v0.2.0"
- and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.559"
+ and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.560"
and (.statements | length) == 56
and ([.statements[].vulnerability.name] | length == (unique | length))
and ([.statements[] | select(.status == "fixed") | .vulnerability.name] | sort)
@@ -2341,7 +2341,7 @@ jq -r '
| @tsv
' "$runtime_vendor_pending" | LC_ALL=C sort -u >"$vendor_pending_fixture"
bash "$vendor_pending_validator" "$runtime_vendor_pending" \
- "$vendor_pending_fixture" v1.6.559 >/dev/null
+ "$vendor_pending_fixture" v1.6.560 >/dev/null
bash scripts/test-vendor-pending-findings.sh >/dev/null
rm -f "$vendor_pending_fixture"
trap - EXIT
@@ -2430,4 +2430,4 @@ for release_readback_contract in \
fi
done
-printf 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.559 base=v1.6.460 engine=0.183.359 web_console=1.6.200 catalog_service=0.20.17 webhook_automation_service=0.10.5 authentication_service=0.4.46 curl=8.18.0-1ubuntu2.8 freemarker=2.3.35 artifact_scan=required vendor_pending=exact-set role_matrix=qa-required locale_layout=qa-required\n'
+printf 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.560 base=v1.6.460 engine=0.183.360 web_console=1.6.201 catalog_service=0.20.17 webhook_automation_service=0.10.5 authentication_service=0.4.46 curl=8.18.0-1ubuntu2.8 freemarker=2.3.35 artifact_scan=required vendor_pending=exact-set role_matrix=qa-required locale_layout=qa-required\n'
diff --git a/scripts/test-current-server-packaging-contract.py b/scripts/test-current-server-packaging-contract.py
index f139ed7b70..f1c07f8a78 100644
--- a/scripts/test-current-server-packaging-contract.py
+++ b/scripts/test-current-server-packaging-contract.py
@@ -34,16 +34,16 @@
PUBLISHED_508 = 'Server `v1.6.508` 已正式發布,封裝 Web Console `1.6.171`。'
STALE_508_CANDIDATE = 'Server `v1.6.508` candidate packages Web Console `1.6.171`'
COMPATIBILITY_CODE = 'SERVER_CREATE_RESPONSE_ORDER_COMPATIBILITY_MISSING'
-WEB_SHA = '97fe46c426cb83263fcfc7f5884c15ca90f7ec3ebec71d9ca023d8b7ad2665c0'
-WEB_SOURCE = '03e0d7b25b8dfcbc45baecf6d448834016aac649'
-WEB_SBOM_SHA = '0ce1f6b99e1e449c545d3d7df65302fdb58d1974fecea6477b20262c2148921c'
-WEB_LOCK_SHA = '1797dd27bb7b1c6b86b8689435e916b03523a9fdf45c5178ec7c2ea05706d4bd'
+WEB_SHA = '5bc8142d9322657c5cbae782756c3aa5a45b6537074de8749066492fa8947eb8'
+WEB_SOURCE = '24715414f143e2c58cf53084bd84cbea0922798f'
+WEB_SBOM_SHA = '8b837eb330b9815df48b4256d3bf0c4e215864b996e2be40dd707ec532872503'
+WEB_LOCK_SHA = 'd255b74a835ab7c0224d12613baacdf021d8b30d46204a71c22d57f35725666f'
CATALOG_COMMIT = '00eec89e5fdeb9728c1c41babccad8b7dfc8b297'
-ENGINE_SHA = '07e7de2548bd98d5209bf3594d69fe5b9a5d7ddba36eed4b6b6e4406022c3f90'
-ENGINE_SOURCE = 'eac3fde5fc7779b6107e2702785120a9c591927b'
+ENGINE_SHA = '0ebbd517ee43e113f29cbc14fe542aa7c7900ae28d65d63a12c348b7f0ac9a21'
+ENGINE_SOURCE = '72230c8b471312377bae968c0c17d63e8ad97ffb'
OLD_COORDINATES = {
- 'v1.6.559': 'v1.6.521',
- '1.6.200': '1.6.181',
+ 'v1.6.560': 'v1.6.521',
+ '1.6.201': '1.6.181',
WEB_SHA: '9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03',
WEB_SOURCE: '7f2bdad0c90a3241693c2ddd4475202ababba550',
CATALOG_COMMIT: '7670ffd81d5f0b5570197fb03c7e55b46da45bf3',
@@ -60,29 +60,29 @@
('LICENSE_SHA256', 'license_sha256', '0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594'),
)
LEGACY_ENGINE_COORDINATES = {
- '0.183.359': '0.183.337',
+ '0.183.360': '0.183.337',
ENGINE_SHA: '7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80',
ENGINE_SOURCE: 'ba041fe312dfe97fc1e5144af576ddcf1b573d8e',
}
EXPECTED = {
COMPATIBILITY_CODE: (COMPATIBILITY, PUBLISHED_508),
- 'SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING': (DOCKER, 'ARG SERVER_RELEASE_TAG=v1.6.559'),
+ 'SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING': (DOCKER, 'ARG SERVER_RELEASE_TAG=v1.6.560'),
'SERVER_INCREMENTAL_RELEASE_VERSION_MISSING': (DOCKER, 'org.opencontainers.image.version="${SERVER_RELEASE_TAG}"'),
'SERVER_INCREMENTAL_RELEASE_RUNTIME_VERSION_MISSING': (DOCKER, 'ENV CATTLE_RANCHER_SERVER_VERSION=${SERVER_RELEASE_TAG}'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_VERSION_MISSING': (DOCKER, 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.200'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_ARTIFACT_MISSING': (DOCKER, 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.200.tar.gz'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_VERSION_MISSING': (DOCKER, 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.201'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_ARTIFACT_MISSING': (DOCKER, 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.201.tar.gz'),
'SERVER_INCREMENTAL_WEB_CONSOLE_HASH_MISSING': (DOCKER, 'ARG WEB_CONSOLE_ARTIFACT_SHA256=' + WEB_SHA),
'SERVER_INCREMENTAL_WEB_CONSOLE_COMMIT_MISSING': (DOCKER, 'ARG WEB_CONSOLE_COMMIT=' + WEB_SOURCE),
'SERVER_INCREMENTAL_WEB_CONSOLE_LOCK_HASH_MISSING': (DOCKER, 'ARG WEB_CONSOLE_LOCK_SHA256=' + WEB_LOCK_SHA),
'SERVER_INCREMENTAL_WEB_CONSOLE_SBOM_HASH_MISSING': (DOCKER, 'ARG WEB_CONSOLE_RUNTIME_SBOM_SHA256=' + WEB_SBOM_SHA),
'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_COMMIT_MISSING': (BUILD, 'web_console_commit=${WEB_CONSOLE_COMMIT:-' + WEB_SOURCE + '}'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_VERSION_MISSING': (BUILD, 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.200}'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_ARTIFACT_MISSING': (BUILD, 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.200.tar.gz}'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_VERSION_MISSING': (BUILD, 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.201}'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_ARTIFACT_MISSING': (BUILD, 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.201.tar.gz}'),
'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_HASH_MISSING': (BUILD, 'web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-' + WEB_SHA + '}'),
'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_LOCK_HASH_MISSING': (BUILD, 'web_console_lock_sha256=${WEB_CONSOLE_LOCK_SHA256:-' + WEB_LOCK_SHA + '}'),
'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_SBOM_HASH_MISSING': (BUILD, 'web_console_runtime_sbom_sha256=${WEB_CONSOLE_RUNTIME_SBOM_SHA256:-' + WEB_SBOM_SHA + '}'),
- 'SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING': (BUILD, 'image=${IMAGE:-pasturestack-validation/server:v1.6.559}'),
- 'SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING': (BUILD, 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.559}'),
+ 'SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING': (BUILD, 'image=${IMAGE:-pasturestack-validation/server:v1.6.560}'),
+ 'SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING': (BUILD, 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.560}'),
'SERVER_INCREMENTAL_RELEASE_BUILD_RUNTIME_VERSION_MISSING': (BUILD, 'CATTLE_RANCHER_SERVER_VERSION="${server_release_tag}"'),
'SERVER_WEB_CONSOLE_RUNTIME_VERSION_GATE_MISSING': (BUILD, 'test "$(cat "${web_root}/VERSION.txt")" = "${WEB_CONSOLE_RELEASE_TAG}"'),
'SERVER_VOLUME_NATIVE_READ_ONLY_BUILD_GATE_MISSING': (DOCKER, 'grep -F \'"volume.isNative" : "r"\' >/dev/null'),
@@ -103,18 +103,18 @@
CREATE_RESPONSE_CODES = ['SERVER_WEB_CREATE_IDENTITY_BUILD_GATE_MISSING','SERVER_WEB_CANONICAL_RECORD_BUILD_GATE_MISSING','SERVER_WEB_CREATE_IDENTITY_RUNTIME_GATE_MISSING','SERVER_WEB_CANONICAL_RECORD_RUNTIME_GATE_MISSING']
ENGINE_EXPECTED = {
'SERVER_INCREMENTAL_ENGINE_REPLACEMENT_MISSING': (DOCKER, 'release_engine_marker', (
- 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.359',
+ 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.360',
'ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar',
'ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=' + ENGINE_SHA,
'ARG ORCHESTRATION_ENGINE_COMMIT=' + ENGINE_SOURCE,
- 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.359',
+ 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.360',
'sh /usr/share/cattle/verify-engine-archive.sh "${archive}" "${ORCHESTRATION_ENGINE_VERSION}" "${ORCHESTRATION_ENGINE_ARTIFACT_SHA256}"',
'cattle-resources-${ORCHESTRATION_ENGINE_VERSION}.jar',
'cattle-app-config-${ORCHESTRATION_ENGINE_VERSION}.jar',
'ENV CATTLE_CATTLE_VERSION=${ORCHESTRATION_ENGINE_RELEASE_TAG}',
)),
'SERVER_INCREMENTAL_ENGINE_BUILD_COORDINATE_MISSING': (BUILD, 'release_engine_build_marker', (
- 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.359}',
+ 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.360}',
'orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar}',
'orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-' + ENGINE_SHA + '}',
'orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-' + ENGINE_SOURCE + '}',
@@ -608,15 +608,15 @@ def verify(files, gate=GATE):
pipeline = r'(?:^|[;\n])\s*' + re.escape(marker) + r'(?=\s*(?:;|\n|$))'
if len(re.findall(pipeline, source)) != 1:
raise AssertionError('CURRENT_CATALOG_HASH_BYPASS')
- if 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.559"' not in gate:
+ if 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.560"' not in gate:
raise AssertionError('CURRENT_VEX_GATE_PIN_MISMATCH')
- if '"$vendor_pending_fixture" v1.6.559 >/dev/null' not in gate:
+ if '"$vendor_pending_fixture" v1.6.560 >/dev/null' not in gate:
raise AssertionError('CURRENT_VENDOR_GATE_PIN_MISMATCH')
- if json.loads(files[VEX]).get('@id') != 'https://github.com/PastureStack/server/security/openvex/v1.6.559':
+ if json.loads(files[VEX]).get('@id') != 'https://github.com/PastureStack/server/security/openvex/v1.6.560':
raise AssertionError('CURRENT_VEX_RELEASE_MISMATCH')
- if json.loads(files[VENDOR]).get('release') != 'v1.6.559':
+ if json.loads(files[VENDOR]).get('release') != 'v1.6.560':
raise AssertionError('CURRENT_VENDOR_RELEASE_MISMATCH')
- if 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.559 base=v1.6.460 engine=0.183.359 web_console=1.6.200 catalog_service=0.20.17 ' not in gate:
+ if 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.560 base=v1.6.460 engine=0.183.360 web_console=1.6.201 catalog_service=0.20.17 ' not in gate:
raise AssertionError('CURRENT_SUMMARY_MISMATCH')
verify_incremental_contract(files, gate)
verify_load_balancer_contract(files, gate)
@@ -631,11 +631,11 @@ def previous_gate(gate=GATE):
if stale(marker) != marker:
replacements["'" + marker + "' "] = "'" + stale(marker) + "' "
replacements.update({
- 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.559"':
+ 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.560"':
'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.521"',
- '"$vendor_pending_fixture" v1.6.559 >/dev/null':
+ '"$vendor_pending_fixture" v1.6.560 >/dev/null':
'"$vendor_pending_fixture" v1.6.521 >/dev/null',
- 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.559 base=v1.6.460 engine=0.183.359 web_console=1.6.200 catalog_service=0.20.17 ':
+ 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.560 base=v1.6.460 engine=0.183.360 web_console=1.6.201 catalog_service=0.20.17 ':
'SERVER_API_EXPLORER_PATCH_OK release=v1.6.521 base=v1.6.460 engine=0.183.340 web_console=1.6.181 catalog_service=0.20.12 ',
})
for marker, stale_marker in replacements.items():
@@ -1299,7 +1299,7 @@ def test_each_old_engine_coordinate_rejected(self):
self.assertGreater(checked, 0)
def test_historical_engine340_cannot_be_packaged(self):
- old = {'0.183.359': '0.183.340',
+ old = {'0.183.360': '0.183.340',
ENGINE_SHA: '5e5a1b908b43bf3824fc3bb2108bd661cd5199ce9bcfd187e0b18b5608a38cea',
ENGINE_SOURCE: 'ff0a41e7676211043eb4f755a306a9e2a19abee1'}
checked = 0
@@ -1320,13 +1320,13 @@ def test_historical_engine340_cannot_be_packaged(self):
def test_engine352_actual_war_producer_and_previous351_350_boundary(self):
candidate = (REPO / 'server/build-component-candidate.sh').read_text(encoding='utf-8')
- self.assertIn('export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.559}', candidate)
- self.assertIn('export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.359}', candidate)
+ self.assertIn('export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.560}', candidate)
+ self.assertIn('export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.360}', candidate)
for old in (
- {'0.183.359': '0.183.351',
+ {'0.183.360': '0.183.351',
ENGINE_SHA: 'efd0e826364c551f782b281f081d53cadb48faa7d5c37c2f101e6f9c7d7f9ea6',
ENGINE_SOURCE: '1befd83278618f9a5b7cdaf96522004dd89918ba'},
- {'0.183.359': '0.183.350',
+ {'0.183.360': '0.183.350',
ENGINE_SHA: '637ddaa42cd93c019b4d0813d7053b71dc42797a7f3ac2aa4b10fcc7e304cb31',
ENGINE_SOURCE: '9c884710e5ea3d4136d80a870d9de9485674f3c3'},
{ENGINE_SOURCE: '109f2623f00ca3a3a9079414a0f0f8bd23cd1eb2'},
@@ -1349,7 +1349,7 @@ def test_engine352_actual_war_producer_and_previous351_350_boundary(self):
def test_immediately_previous_engine341_cannot_be_packaged(self):
- old = {'0.183.359': '0.183.341',
+ old = {'0.183.360': '0.183.341',
ENGINE_SHA: '3ede1ad28b7a7e0754c4c953bd486162f7ffe0b2f76ce384ec485632af6b8e6d',
ENGINE_SOURCE: 'b245cf778479445ded0786d3898b03fea153212e'}
checked = 0
@@ -1370,7 +1370,7 @@ def test_immediately_previous_engine341_cannot_be_packaged(self):
def test_immediately_previous_engine342_cannot_be_packaged(self):
- old = {'0.183.359': '0.183.342',
+ old = {'0.183.360': '0.183.342',
ENGINE_SHA: '3533f247392795329c92516a7966510aafbe81a52e3f922e5ec657a2071363b5',
ENGINE_SOURCE: 'ddae4bb8e12806f70816f5782cd999eccd0f2ce6'}
checked = 0
@@ -1391,7 +1391,7 @@ def test_immediately_previous_engine342_cannot_be_packaged(self):
def test_immediately_previous_engine343_cannot_be_packaged(self):
- old = {'0.183.359': '0.183.343',
+ old = {'0.183.360': '0.183.343',
ENGINE_SHA: 'b29c0057f96dbae38bfe2c663c63bbe4c8b5c755b3090f39414aa51f9813025f',
ENGINE_SOURCE: '5aa9d8978c75a7fe63330453c399a92716d88568'}
checked = 0
@@ -1412,7 +1412,7 @@ def test_immediately_previous_engine343_cannot_be_packaged(self):
def test_immediately_previous_engine344_cannot_be_packaged(self):
- old = {'0.183.359': '0.183.344',
+ old = {'0.183.360': '0.183.344',
ENGINE_SHA: 'ffafa4700156b74ec11bcae1b830738e49943f1a3b8b0565895ed6d7726eebf1',
ENGINE_SOURCE: '72c42e6f2000301fe03ffd2442fb9ee89aee120c'}
checked = 0
@@ -1432,7 +1432,7 @@ def test_immediately_previous_engine344_cannot_be_packaged(self):
self.assertGreater(checked, 0)
def test_immediately_previous_engine345_cannot_be_packaged(self):
- old = {'0.183.359': '0.183.345',
+ old = {'0.183.360': '0.183.345',
ENGINE_SHA: '780263c4a884527fa80a28b998ad4bda88b9ad6002a687f9f074602ecad46e62',
ENGINE_SOURCE: 'd27d4ac73fa05ae43b9ae0c32875780e8a3765b1'}
checked = 0
@@ -1452,7 +1452,7 @@ def test_immediately_previous_engine345_cannot_be_packaged(self):
self.assertGreater(checked, 0)
def test_immediately_previous_engine346_cannot_be_packaged(self):
- old = {'0.183.359': '0.183.346',
+ old = {'0.183.360': '0.183.346',
ENGINE_SHA: '9deeda5758b7f04135441a203092dd0308d90b65d25037b62ef4f9361d3cf806',
ENGINE_SOURCE: 'e250e4a17958216c30c1f5d0d31f7b6418a968d5'}
checked = 0
@@ -1628,7 +1628,7 @@ def test_machine_actual_binary_package_and_retired_boundary_are_verified(self):
def test_current_readme_status_specific_prose_does_not_block_publication_transition(self):
common = GATE.split('for current_readme_marker in ', 1)[1].split('; do', 1)[0]
self.assertNotIn('candidate', common)
- self.assertNotIn('v1.6.559', common)
+ self.assertNotIn('v1.6.560', common)
self.assertIn("grep -Fqi 'keep all three named volumes' README.md", GATE)
status = GATE.split("if grep -Fxq '## Current source candidate' README.md; then", 1)[1]
candidate, published = status.split("elif grep -Fxq '## Current release' README.md; then", 1)
@@ -1637,7 +1637,7 @@ def test_current_readme_status_specific_prose_does_not_block_publication_transit
self.assertIn('This candidate is not yet available', candidate)
self.assertIn('SERVER_CURRENT_PUBLISHED_README_MISSING', published)
self.assertNotIn('candidate', published)
- self.assertNotIn('v1.6.559', published)
+ self.assertNotIn('v1.6.560', published)
if __name__ == '__main__':
diff --git a/server/Dockerfile.web-compose-release b/server/Dockerfile.web-compose-release
index 5eb8bfb2c5..4c38c4550d 100644
--- a/server/Dockerfile.web-compose-release
+++ b/server/Dockerfile.web-compose-release
@@ -90,13 +90,13 @@ ARG PER_HOST_SUBNET_STARTUP_SHA256=f22b41f18d1733c037babed4d2e0474efeedc6017a968
ARG PER_HOST_SUBNET_PROVENANCE_SHA256=afa2db0e0755c6bd3ee28e111937c521a6d84e6d71d563e99202cf4693000eaf
ARG WEB_CONSOLE_RELEASE_BASE_URL=https://github.com/PastureStack/web-console/releases/download
-ARG WEB_CONSOLE_RELEASE_TAG=1.6.200
-ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.200.tar.gz
-ARG WEB_CONSOLE_ARTIFACT_SHA256=97fe46c426cb83263fcfc7f5884c15ca90f7ec3ebec71d9ca023d8b7ad2665c0
-ARG WEB_CONSOLE_COMMIT=03e0d7b25b8dfcbc45baecf6d448834016aac649
-ARG WEB_CONSOLE_LOCK_SHA256=1797dd27bb7b1c6b86b8689435e916b03523a9fdf45c5178ec7c2ea05706d4bd
-ARG WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT=web-console-1.6.200-runtime.cdx.json
-ARG WEB_CONSOLE_RUNTIME_SBOM_SHA256=0ce1f6b99e1e449c545d3d7df65302fdb58d1974fecea6477b20262c2148921c
+ARG WEB_CONSOLE_RELEASE_TAG=1.6.201
+ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.201.tar.gz
+ARG WEB_CONSOLE_ARTIFACT_SHA256=5bc8142d9322657c5cbae782756c3aa5a45b6537074de8749066492fa8947eb8
+ARG WEB_CONSOLE_COMMIT=24715414f143e2c58cf53084bd84cbea0922798f
+ARG WEB_CONSOLE_LOCK_SHA256=d255b74a835ab7c0224d12613baacdf021d8b30d46204a71c22d57f35725666f
+ARG WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT=web-console-1.6.201-runtime.cdx.json
+ARG WEB_CONSOLE_RUNTIME_SBOM_SHA256=8b837eb330b9815df48b4256d3bf0c4e215864b996e2be40dd707ec532872503
ARG CATALOG_SERVICE_RELEASE_BASE_URL=https://github.com/PastureStack/catalog-service/releases/download
ARG CATALOG_SERVICE_VERSION=0.20.17
ARG CATALOG_SERVICE_COMMIT=2f70559e340865eb6243662ed33d8cbc7b1efa90
@@ -118,9 +118,9 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.5
ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=f2df8874d5a00a2d5713594609063256164ed624d29a733149d00cb271f0f66b
ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=b98e55db2739ab45119eacc94c8f65cb44371480ebcebec95e5470fd67c72a4c
ARG ORCHESTRATION_ENGINE_RELEASE_BASE_URL=https://github.com/PastureStack/orchestration-engine/releases/download
-ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.359
+ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.360
ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar
-ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=07e7de2548bd98d5209bf3594d69fe5b9a5d7ddba36eed4b6b6e4406022c3f90
+ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=0ebbd517ee43e113f29cbc14fe542aa7c7900ae28d65d63a12c348b7f0ac9a21
ARG COMPOSE_EXECUTOR_RELEASE_BASE_URL=https://github.com/PastureStack/compose-cli/releases/download
ARG COMPOSE_EXECUTOR_VERSION=0.14.40
ARG COMPOSE_EXECUTOR_ARCHIVE_SHA256=fa7a4127793ef4e8e6ef18d926d74b50046a80dd2097b08878ef576ddfce6732
@@ -709,12 +709,12 @@ ARG PER_HOST_SUBNET_VERSION=0.2.5
ARG PER_HOST_SUBNET_PRODUCER_COMMIT=e102e0bb38d84b0a9cbf87e5458cf1bc74d01904
ARG PER_HOST_SUBNET_ARCHIVE_SHA256=7237e83e96a64e7f3de64eb86bff9721c4e879c35cda9e9de1e4cb1ba225f298
ARG PER_HOST_SUBNET_PROVENANCE_SHA256=afa2db0e0755c6bd3ee28e111937c521a6d84e6d71d563e99202cf4693000eaf
-ARG SERVER_RELEASE_TAG=v1.6.559
+ARG SERVER_RELEASE_TAG=v1.6.560
ARG API_EXPLORER_RELEASE_TAG=v1.1.20
ARG API_EXPLORER_ARTIFACT_SHA256=c989ee12a518f6d9d2b23b1f17f48d781aacf6ec31fb65291dd7de9b1661badb
ARG API_EXPLORER_COMMIT=a586f0dc0aa50df579049a8d4bf26f04400fdf75
-ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.359
-ARG ORCHESTRATION_ENGINE_VERSION=0.183.359
+ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.360
+ARG ORCHESTRATION_ENGINE_VERSION=0.183.360
ARG ENGINE_READONLY_SCHEMA_SHA256=7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233
ARG ENGINE_RESTRICTED_SCHEMA_SHA256=f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51
ARG HOST_API_VERSION=0.38.8
@@ -736,11 +736,11 @@ ARG GLIBC_PACKAGE_VERSION=2.43-2ubuntu2.4
ARG GLIBC_RUNTIME_PACKAGE_VERSION=2.43-2ubuntu2.4+pasturestack.1
ARG PERL_PACKAGE_VERSION=5.40.1-7ubuntu0.3
ARG PCRE2_PACKAGE_VERSION=10.46-1build1+pasturestack.1
-ARG WEB_CONSOLE_RELEASE_TAG=1.6.200
-ARG WEB_CONSOLE_ARTIFACT_SHA256=97fe46c426cb83263fcfc7f5884c15ca90f7ec3ebec71d9ca023d8b7ad2665c0
-ARG WEB_CONSOLE_COMMIT=03e0d7b25b8dfcbc45baecf6d448834016aac649
-ARG WEB_CONSOLE_LOCK_SHA256=1797dd27bb7b1c6b86b8689435e916b03523a9fdf45c5178ec7c2ea05706d4bd
-ARG WEB_CONSOLE_RUNTIME_SBOM_SHA256=0ce1f6b99e1e449c545d3d7df65302fdb58d1974fecea6477b20262c2148921c
+ARG WEB_CONSOLE_RELEASE_TAG=1.6.201
+ARG WEB_CONSOLE_ARTIFACT_SHA256=5bc8142d9322657c5cbae782756c3aa5a45b6537074de8749066492fa8947eb8
+ARG WEB_CONSOLE_COMMIT=24715414f143e2c58cf53084bd84cbea0922798f
+ARG WEB_CONSOLE_LOCK_SHA256=d255b74a835ab7c0224d12613baacdf021d8b30d46204a71c22d57f35725666f
+ARG WEB_CONSOLE_RUNTIME_SBOM_SHA256=8b837eb330b9815df48b4256d3bf0c4e215864b996e2be40dd707ec532872503
ARG CATALOG_SERVICE_VERSION=0.20.17
ARG CATALOG_SERVICE_COMMIT=2f70559e340865eb6243662ed33d8cbc7b1efa90
ARG CATALOG_SERVICE_ARCHIVE_SHA256=4f83f89763ce325262baf8984782da2c21fd81e3cc22c0a1c76aaa50b2a50f20
@@ -759,8 +759,8 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.5
ARG WEBHOOK_AUTOMATION_SERVICE_COMMIT=4da77ccac464bc7f7cd2020e0659a666a5d1ff7e
ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=f2df8874d5a00a2d5713594609063256164ed624d29a733149d00cb271f0f66b
ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=b98e55db2739ab45119eacc94c8f65cb44371480ebcebec95e5470fd67c72a4c
-ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=07e7de2548bd98d5209bf3594d69fe5b9a5d7ddba36eed4b6b6e4406022c3f90
-ARG ORCHESTRATION_ENGINE_COMMIT=eac3fde5fc7779b6107e2702785120a9c591927b
+ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=0ebbd517ee43e113f29cbc14fe542aa7c7900ae28d65d63a12c348b7f0ac9a21
+ARG ORCHESTRATION_ENGINE_COMMIT=72230c8b471312377bae968c0c17d63e8ad97ffb
ARG HOST_PROVISIONER_VERSION=0.39.10
ARG HOST_PROVISIONER_COMMIT=d5f6d5c9f4f0599f4bf1bdc9d5d0cca2e2e0c1ba
ARG HOST_PROVISIONER_ARCHIVE_SHA256=fd8a814c5810746091ba3192a9fd31699e6865a2598654b9bbb55eae90bef661
diff --git a/server/build-api-explorer-patch-image.sh b/server/build-api-explorer-patch-image.sh
index 65683b8bf5..ac51a8cd81 100755
--- a/server/build-api-explorer-patch-image.sh
+++ b/server/build-api-explorer-patch-image.sh
@@ -14,14 +14,14 @@ if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
fi
revision=${PASTURESTACK_SERVER_REVISION:-$(git rev-parse HEAD)}
-server_release_tag=${SERVER_RELEASE_TAG:-v1.6.559}
+server_release_tag=${SERVER_RELEASE_TAG:-v1.6.560}
source_date_epoch=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)}
base_image=${BASE_IMAGE:-ghcr.io/pasturestack/server:v1.6.460@sha256:c855af8aea232dacc5bb6df68e2271d482c68b53c43ab0c108ec19118f5ab403}
orchestration_engine_release_base_url=${ORCHESTRATION_ENGINE_RELEASE_BASE_URL:-https://github.com/PastureStack/orchestration-engine/releases/download}
-orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.359}
+orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.360}
orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar}
-orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-07e7de2548bd98d5209bf3594d69fe5b9a5d7ddba36eed4b6b6e4406022c3f90}
-orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-eac3fde5fc7779b6107e2702785120a9c591927b}
+orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-0ebbd517ee43e113f29cbc14fe542aa7c7900ae28d65d63a12c348b7f0ac9a21}
+orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-72230c8b471312377bae968c0c17d63e8ad97ffb}
orchestration_engine_version=${orchestration_engine_release_tag#v}
engine_readonly_schema_sha256=${ENGINE_READONLY_SCHEMA_SHA256:-7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233}
engine_restricted_schema_sha256=${ENGINE_RESTRICTED_SCHEMA_SHA256:-f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51}
@@ -63,13 +63,13 @@ api_explorer_artifact=${API_EXPLORER_ARTIFACT:-api-explorer-1.1.20.tar.gz}
api_explorer_artifact_sha256=${API_EXPLORER_ARTIFACT_SHA256:-c989ee12a518f6d9d2b23b1f17f48d781aacf6ec31fb65291dd7de9b1661badb}
api_explorer_commit=${API_EXPLORER_COMMIT:-a586f0dc0aa50df579049a8d4bf26f04400fdf75}
web_console_release_base_url=${WEB_CONSOLE_RELEASE_BASE_URL:-https://github.com/PastureStack/web-console/releases/download}
-web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.200}
-web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.200.tar.gz}
-web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-97fe46c426cb83263fcfc7f5884c15ca90f7ec3ebec71d9ca023d8b7ad2665c0}
-web_console_commit=${WEB_CONSOLE_COMMIT:-03e0d7b25b8dfcbc45baecf6d448834016aac649}
-web_console_lock_sha256=${WEB_CONSOLE_LOCK_SHA256:-1797dd27bb7b1c6b86b8689435e916b03523a9fdf45c5178ec7c2ea05706d4bd}
+web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.201}
+web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.201.tar.gz}
+web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-5bc8142d9322657c5cbae782756c3aa5a45b6537074de8749066492fa8947eb8}
+web_console_commit=${WEB_CONSOLE_COMMIT:-24715414f143e2c58cf53084bd84cbea0922798f}
+web_console_lock_sha256=${WEB_CONSOLE_LOCK_SHA256:-d255b74a835ab7c0224d12613baacdf021d8b30d46204a71c22d57f35725666f}
web_console_runtime_sbom_artifact=${WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT:-web-console-${web_console_release_tag}-runtime.cdx.json}
-web_console_runtime_sbom_sha256=${WEB_CONSOLE_RUNTIME_SBOM_SHA256:-0ce1f6b99e1e449c545d3d7df65302fdb58d1974fecea6477b20262c2148921c}
+web_console_runtime_sbom_sha256=${WEB_CONSOLE_RUNTIME_SBOM_SHA256:-8b837eb330b9815df48b4256d3bf0c4e215864b996e2be40dd707ec532872503}
catalog_service_release_base_url=${CATALOG_SERVICE_RELEASE_BASE_URL:-https://github.com/PastureStack/catalog-service/releases/download}
catalog_service_version=${CATALOG_SERVICE_VERSION:-0.20.17}
catalog_service_commit=${CATALOG_SERVICE_COMMIT:-2f70559e340865eb6243662ed33d8cbc7b1efa90}
@@ -130,7 +130,7 @@ usage_telemetry_agent_archive_sha256=${USAGE_TELEMETRY_AGENT_ARCHIVE_SHA256:-6d4
usage_telemetry_agent_binary_sha256=${USAGE_TELEMETRY_AGENT_BINARY_SHA256:-90ed65f689c91801abd0cf482211c96a01e7c63ef695e4faecdf4713eff730be}
supported_docker_range='~v1.12.3 || ~v1.13.0 || ~v17.03.0 || ~v17.06.0 || ~v17.09.0 || ~v17.12.0 || ~v18.03.0 || ~v18.06.0 || ~v18.09.0 || ~v19.03.2 || v24.0.9 || >=v29.4.1 <=v29.7.2 || v29.8.0'
newest_docker_version=v29.8.0
-image=${IMAGE:-pasturestack-validation/server:v1.6.559}
+image=${IMAGE:-pasturestack-validation/server:v1.6.560}
build_options=()
[[ "$revision" =~ ^[0-9a-f]{40}$ ]]
diff --git a/server/build-component-candidate.sh b/server/build-component-candidate.sh
index 609f482195..0677de05d5 100755
--- a/server/build-component-candidate.sh
+++ b/server/build-component-candidate.sh
@@ -18,10 +18,10 @@ for name in "${required[@]}"; do
case "$name" in *_COMMIT) [[ ${!name} =~ ^[0-9a-f]{40}$ ]] ;; *) [[ ${!name} =~ ^[0-9a-f]{64}$ ]] ;; esac
export "$name"
done
-export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.559}
-export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.359}
+export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.560}
+export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.360}
export ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar
-export WEB_CONSOLE_RELEASE_TAG=${WEB_CONSOLE_RELEASE_TAG:-1.6.200}
+export WEB_CONSOLE_RELEASE_TAG=${WEB_CONSOLE_RELEASE_TAG:-1.6.201}
export WEB_CONSOLE_ARTIFACT="web-console-${WEB_CONSOLE_RELEASE_TAG}.tar.gz"
export WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT=${WEB_CONSOLE_RUNTIME_SBOM_ARTIFACT:-web-console-${WEB_CONSOLE_RELEASE_TAG}-runtime.cdx.json}
export WEBSOCKET_PROXY_VERSION=${WEBSOCKET_PROXY_VERSION:-0.23.18}
diff --git a/server/console-broker/ai_audit.go b/server/console-broker/ai_audit.go
new file mode 100644
index 0000000000..16b5a97d11
--- /dev/null
+++ b/server/console-broker/ai_audit.go
@@ -0,0 +1,218 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "time"
+)
+
+const aiAuditQueryPath = "/v2-beta/pasturestack/ai-audit-queries"
+
+var aiAuditToken = regexp.MustCompile("^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$")
+
+type aiAuditReadContext struct{}
+
+func isAIAuditRead(ctx context.Context) bool {
+ return ctx.Value(aiAuditReadContext{}) == true
+}
+
+// Fixed read workflow. Locators remain subject to the original audit owner;
+// neither tool references nor caller-selected identity headers grant access.
+type aiAuditInput struct {
+ KeyID string `json:"keyId,omitempty"`
+ EnvironmentID string `json:"environmentId,omitempty"`
+ From string `json:"from,omitempty"`
+ To string `json:"to,omitempty"`
+ LastMinutes *int `json:"lastMinutes,omitempty"`
+ Decision string `json:"decision,omitempty"`
+ Outcome string `json:"outcome,omitempty"`
+ Operation string `json:"operation,omitempty"`
+ TargetType string `json:"targetType,omitempty"`
+ RequestID string `json:"requestId,omitempty"`
+ HTTPStatus *int `json:"httpStatus,omitempty"`
+ Limit *int `json:"limit,omitempty"`
+ Offset *int `json:"offset,omitempty"`
+ EventID string `json:"eventId,omitempty"`
+}
+
+func aiAuditInvalid() error {
+ return &auditHTTPError{Status: http.StatusBadRequest, Code: "invalid_ai_audit_query", Message: "Use typed audit filters and a bounded time range"}
+}
+
+func (input aiAuditInput) query(now time.Time) (auditQuery, error) {
+ values := url.Values{"sort": {"created"}, "order": {"desc"}, "limit": {"50"}}
+ for name, value := range map[string]string{
+ "keyId": input.KeyID, "accountId": input.EnvironmentID, "eventId": input.EventID,
+ "decision": input.Decision, "outcome": input.Outcome, "operation": input.Operation,
+ "targetType": input.TargetType, "requestId": input.RequestID,
+ } {
+ if value != "" {
+ if !aiAuditToken.MatchString(value) {
+ return auditQuery{}, aiAuditInvalid()
+ }
+ values.Set(name, value)
+ }
+ }
+ if input.Decision != "" && input.Decision != "ALLOW" && input.Decision != "DENY" && input.Decision != "NOT_EVALUATED" {
+ return auditQuery{}, aiAuditInvalid()
+ }
+ if input.LastMinutes != nil {
+ if *input.LastMinutes < 1 || *input.LastMinutes > 366*24*60 || input.From != "" || input.To != "" {
+ return auditQuery{}, aiAuditInvalid()
+ }
+ input.From = now.Add(-time.Duration(*input.LastMinutes) * time.Minute).Format(time.RFC3339Nano)
+ input.To = now.Format(time.RFC3339Nano)
+ }
+ if input.From != "" || input.To != "" {
+ values.Set("timeScope", "range")
+ values.Set("created_gte", input.From)
+ values.Set("created_lte", input.To)
+ }
+ if input.Limit != nil {
+ if *input.Limit < 1 || *input.Limit > 50 {
+ return auditQuery{}, aiAuditInvalid()
+ }
+ values.Set("limit", strconv.Itoa(*input.Limit))
+ }
+ if input.Offset != nil {
+ values.Set("offset", strconv.Itoa(*input.Offset))
+ }
+ if input.HTTPStatus != nil {
+ if *input.HTTPStatus < 100 || *input.HTTPStatus > 599 {
+ return auditQuery{}, aiAuditInvalid()
+ }
+ values.Set("httpStatus", strconv.Itoa(*input.HTTPStatus))
+ }
+ return parseAuditQuery(values, now)
+}
+
+func (b *broker) serveAIAudit(writer http.ResponseWriter, request *http.Request) {
+ if request.Method != http.MethodPost {
+ writer.Header().Set("Allow", "POST")
+ writeJSONError(writer, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
+ return
+ }
+ // Presence restricts this first adapter to native Cookie transport. Only
+ // the existing Engine/native owner below authenticates that cookie.
+ cookie := request.Header.Get("Cookie")
+ if auditOwner(request.Context()) == nil && (cookie == "" || len(cookie) > 32768 || request.Header.Get("Authorization") != "") {
+ writeJSONError(writer, http.StatusForbidden, "ai_audit_cookie_required", "This audit workflow requires the current native browser session")
+ return
+ }
+ var input *aiAuditInput
+ decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 8192))
+ decoder.DisallowUnknownFields()
+ if request.URL.RawQuery != "" || decoder.Decode(&input) != nil || input == nil || decoder.Decode(&struct{}{}) != io.EOF {
+ writeAuditFailure(writer, aiAuditInvalid())
+ return
+ }
+ query, err := input.query(time.Now().UTC())
+ if err != nil {
+ writeAuditFailure(writer, err)
+ return
+ }
+ // No caller actor/project/forwarded-identity header is forwarded as a grant.
+ native := request.Clone(context.WithValue(request.Context(), aiAuditReadContext{}, true))
+ native.Header = make(http.Header)
+ native.Header.Set("Cookie", cookie)
+ if auditOwner(request.Context()) != nil && request.Header.Get("Authorization") != "" {
+ native.Header.Set("Authorization", request.Header.Get("Authorization"))
+ }
+ if input.KeyID != "" {
+ owner, viewer, err := b.fetchKeyAuditAuthority(native.Context(), native, input.KeyID)
+ if err != nil {
+ writeAuditFailure(writer, err)
+ return
+ }
+ query.KeyID, query.KeyOwnerAccountID, query.ViewerAccountID = input.KeyID, owner, viewer
+ }
+ result, err := b.runAuditQuery(native.Context(), native, query)
+ if err != nil {
+ writeAuditFailure(writer, err)
+ return
+ }
+ if input.EventID != "" {
+ found := make([]map[string]any, 0, 1)
+ for _, row := range result.Records {
+ if auditString(row, "id") == input.EventID {
+ found = append(found, row)
+ }
+ }
+ if len(found) != 1 {
+ writeJSONError(writer, http.StatusNotFound, "ai_audit_event_unavailable", "The event is unavailable under this query and current permissions")
+ return
+ }
+ result.Records, result.Total, query.Offset = found, 1, 0
+ }
+ start := query.Offset
+ if start > result.Total {
+ start = result.Total
+ }
+ end := start + query.Limit
+ if end > result.Total {
+ end = result.Total
+ }
+ rows := make([]map[string]any, 0, end-start)
+ for _, row := range result.Records[start:end] {
+ projected := safeAIAuditRecord(row, input.KeyID)
+ if !aiAuditToken.MatchString(auditString(projected, "id")) {
+ writeJSONError(writer, http.StatusBadGateway, "invalid_audit_response", "The audit owner returned an invalid event identity")
+ return
+ }
+ rows = append(rows, projected)
+ }
+ var next any
+ if end < result.Total {
+ next = end
+ }
+ writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+ writer.Header().Set("Cache-Control", "no-store")
+ writer.Header().Set("X-Content-Type-Options", "nosniff")
+ _ = json.NewEncoder(writer).Encode(map[string]any{
+ "resourceType": "auditLog", "data": rows, "total": result.Total, "nextOffset": next,
+ "range": map[string]string{"from": query.From.Format(time.RFC3339Nano), "to": query.To.Format(time.RFC3339Nano)},
+ "payloadPolicy": "HUMAN_ONLY_NATIVE_AUDIT_UI",
+ })
+}
+
+func safeAIAuditRecord(record map[string]any, keyID string) map[string]any {
+ metadata := safeKeyAuditRecord(record, keyID)
+ result := map[string]any{"type": "auditLog", "payloadsIncluded": false}
+ for _, field := range []string{
+ "id", "accountId", "authenticatedAsAccountId", "keyId", "eventType", "resourceType", "resourceId",
+ "authType", "decision", "outcome", "responseCode", "requestId", "targetType", "targetId",
+ "operation", "phase", "processId", "processName", "failureCode",
+ } {
+ if value := auditString(metadata, field); aiAuditToken.MatchString(value) {
+ result[field] = value
+ }
+ }
+ if created, valid := auditTimestamp(record); valid {
+ result["created"] = created.UTC().Format(time.RFC3339Nano)
+ }
+ for _, field := range []string{"httpStatus", "policyRevision"} {
+ value, err := strconv.ParseInt(auditString(metadata, field), 10, 64)
+ if err == nil && value >= 0 && value <= 9007199254740991 {
+ result[field] = value
+ }
+ }
+ if value, ok := metadata["preview"].(bool); ok {
+ result["preview"] = value
+ }
+ for _, field := range []string{"request", "response"} {
+ state := "not_projected"
+ if value, present := record[field+"Object"]; present && value != nil {
+ state = "present"
+ }
+ result[field+"PayloadPresence"] = state
+ }
+ if value := auditInteractionChannel(record); validAuditChannel(value) {
+ result["interactionChannel"] = value
+ }
+ return result
+}
diff --git a/server/console-broker/ai_audit_cookie_owner_test.go b/server/console-broker/ai_audit_cookie_owner_test.go
new file mode 100644
index 0000000000..eda1ad2618
--- /dev/null
+++ b/server/console-broker/ai_audit_cookie_owner_test.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+)
+
+func TestAIAuditCookieOwnerUsesSameSafeSourcesWithoutBorrowingAnyKey(t *testing.T) {
+ foreignKey := false
+ b := ownerTestBroker(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" || r.URL.Path != "/v2-beta/aiResourceViews" || r.Header.Get("Cookie") != "R_SESS=PRIVATE" ||
+ r.Header.Get("Authorization") != "" || r.Header.Get("X-PastureStack-Key-Expected-Revision") != "" {
+ t.Error("cookie authority changed")
+ w.WriteHeader(500)
+ return
+ }
+ var input map[string]any
+ _ = json.NewDecoder(r.Body).Decode(&input)
+ category := auditString(input, "resourceType")
+ page := input["sourcePage"].(map[string]any)
+ project := auditString(page, "projectId")
+ authority := map[string]any{"actorId": "1a1", "accountId": firstNonEmpty(project, "1a1")}
+ if foreignKey {
+ authority["credentialId"] = "1c99"
+ }
+ rows := []map[string]any{}
+ if category == "project" {
+ rows = append(rows, map[string]any{"id": "1a5", "type": "project"})
+ }
+ if category == "auditLog" && project != "" {
+ rows = append(rows, map[string]any{"id": "1al10", "accountId": "1a5", "created": "2026-10-01T12:00:00Z"})
+ }
+ _ = json.NewEncoder(w).Encode(map[string]any{"resourceType": category, "data": rows, "nextMarker": nil,
+ "sourcePageResult": true, "nativeAuthority": authority})
+ }))
+ input := ownerInput()
+ input.Authorization = ""
+ input.Cookie = "R_SESS=PRIVATE"
+ input.CredentialID = ""
+ input.CredentialKind = ""
+ input.Revision = 0
+ status, body, _ := ownerFrame(t, b, input)
+ if status != 200 || body["total"] != float64(1) {
+ t.Fatal("cookie metadata workflow failed")
+ }
+ foreignKey = true
+ status, body, _ = ownerFrame(t, b, input)
+ if status == 200 || body["data"] != nil {
+ t.Fatal("cookie query borrowed another credential")
+ }
+}
diff --git a/server/console-broker/ai_audit_owner.go b/server/console-broker/ai_audit_owner.go
new file mode 100644
index 0000000000..86b815a031
--- /dev/null
+++ b/server/console-broker/ai_audit_owner.go
@@ -0,0 +1,271 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// This envelope is accepted only by the fixed local CLI, never by an HTTP route.
+// Credentials arrive through private stdin, not argv, environment or logs.
+type aiAuditOwnerInput struct {
+ Query *aiAuditInput `json:"query"`
+ ActorID string `json:"actorId"`
+ Cookie string `json:"cookie,omitempty"`
+ Authorization string `json:"authorization,omitempty"`
+ CredentialID string `json:"credentialId,omitempty"`
+ CredentialKind string `json:"credentialKind,omitempty"`
+ Revision int64 `json:"revision,omitempty"`
+}
+
+type aiAuditOwnerContext struct{}
+type aiAuditOwnerState struct {
+ input aiAuditOwnerInput
+}
+
+func auditOwner(ctx context.Context) *aiAuditOwnerState {
+ state, _ := ctx.Value(aiAuditOwnerContext{}).(*aiAuditOwnerState)
+ return state
+}
+
+type boundedAuditResponse struct {
+ header http.Header
+ status int
+ body bytes.Buffer
+}
+
+func (w *boundedAuditResponse) Header() http.Header { return w.header }
+func (w *boundedAuditResponse) WriteHeader(status int) {
+ if w.status == 0 {
+ w.status = status
+ }
+}
+func (w *boundedAuditResponse) Write(data []byte) (int, error) {
+ if w.status == 0 {
+ w.status = 200
+ }
+ if w.body.Len()+len(data) > 512*1024 {
+ return 0, errors.New("audit output bound")
+ }
+ return w.body.Write(data)
+}
+
+func runAIAuditOwnerCLI(stdin io.Reader, stdout io.Writer) int {
+ var input *aiAuditOwnerInput
+ decoder := json.NewDecoder(io.LimitReader(stdin, 65537))
+ decoder.DisallowUnknownFields()
+ if decoder.Decode(&input) != nil || input == nil || decoder.Decode(&struct{}{}) != io.EOF ||
+ input.Query == nil || !keyAuditIDPattern.MatchString(input.ActorID) ||
+ (input.Cookie == "") == (input.Authorization == "") || len(input.Cookie)+len(input.Authorization) > 32768 ||
+ input.Authorization != "" && (!strings.HasPrefix(input.Authorization, "Basic ") ||
+ input.Revision < 1 || !keyAuditIDPattern.MatchString(input.CredentialID) ||
+ input.CredentialKind != "apiKey" && input.CredentialKind != "apiKeyRestricted") {
+ return 2 // No private input or diagnostic is printed.
+ }
+ cfg := brokerConfig{UpstreamURL: "http://127.0.0.1:8082", SessionDialURL: "http://127.0.0.1:8080",
+ MaxSessions: 1, ReplayBytes: 65536, ActiveTTL: time.Hour, HistoryTTL: time.Hour, CleanupInterval: time.Hour}
+ b, err := newBroker(cfg, log.New(io.Discard, "", 0))
+ if err != nil {
+ return 2
+ }
+ defer b.close()
+ return executeAIAuditOwner(b, *input, stdout)
+}
+
+func executeAIAuditOwner(b *broker, input aiAuditOwnerInput, stdout io.Writer) int {
+ ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
+ defer cancel()
+ ctx = context.WithValue(ctx, aiAuditOwnerContext{}, &aiAuditOwnerState{input: input})
+ payload, err := json.Marshal(input.Query)
+ if err != nil {
+ return 2
+ }
+ request, _ := http.NewRequestWithContext(ctx, "POST", "http://127.0.0.1"+aiAuditQueryPath, bytes.NewReader(payload))
+ if input.Cookie != "" {
+ request.Header.Set("Cookie", input.Cookie)
+ }
+ if input.Authorization != "" {
+ request.Header.Set("Authorization", input.Authorization)
+ }
+ response := &boundedAuditResponse{header: make(http.Header)}
+ b.serveAIAudit(response, request)
+ var body map[string]any
+ if json.Unmarshal(response.body.Bytes(), &body) != nil {
+ return 2
+ }
+ if json.NewEncoder(stdout).Encode(map[string]any{"status": response.status, "body": body}) != nil {
+ return 2
+ }
+ return 0
+}
+
+// Every source is still the native manager's current role/Key read. This adapter
+// has no SQL, service credential or raw-payload fallback.
+func (b *broker) fetchAIAuditSource(ctx context.Context, incoming *http.Request, category, resourceID, projectID, keyKind string, values url.Values) (auditUpstreamCollection, error) {
+ state := auditOwner(ctx)
+ if state == nil {
+ return auditUpstreamCollection{}, aiAuditInvalid()
+ }
+ page := map[string]any{"limit": 1000}
+ for native, typed := range map[string]string{"marker": "marker", "projectId": "projectId", "accountId": "accountId", "created_gte": "from", "created_lte": "to"} {
+ if value := values.Get(native); value != "" {
+ page[typed] = value
+ }
+ }
+ if projectID != "" {
+ page["projectId"] = projectID
+ }
+ if keyKind != "" {
+ page["keyKind"] = keyKind
+ }
+ body := map[string]any{"resourceType": category, "sourcePage": page}
+ if resourceID != "" {
+ body["resourceId"] = resourceID
+ }
+ payload, err := json.Marshal(body)
+ if err != nil {
+ return auditUpstreamCollection{}, aiAuditInvalid()
+ }
+ target := *b.upstreamURL
+ target.Path = "/v2-beta/aiResourceViews"
+ target.RawPath = ""
+ target.RawQuery = ""
+ target.Fragment = ""
+ request, err := http.NewRequestWithContext(ctx, "POST", target.String(), bytes.NewReader(payload))
+ if err != nil {
+ return auditUpstreamCollection{}, aiAuditInvalid()
+ }
+ request.Header.Set("Content-Type", "application/json")
+ if state.input.Cookie != "" {
+ request.Header.Set("Cookie", state.input.Cookie)
+ }
+ if state.input.Authorization != "" {
+ request.Header.Set("Authorization", state.input.Authorization)
+ request.Header.Set("X-PastureStack-Key-Expected-Revision", strconv.FormatInt(state.input.Revision, 10))
+ }
+ client := &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("audit source redirect denied") }}
+ response, err := client.Do(request)
+ if err != nil {
+ return auditUpstreamCollection{}, &auditHTTPError{Status: 502, Code: "audit_upstream_unavailable", Message: "Audit source unavailable"}
+ }
+ defer response.Body.Close()
+ if response.StatusCode != 200 && response.StatusCode != 201 {
+ status := response.StatusCode
+ if status != 401 && status != 403 && status != 404 && status != 422 {
+ status = 502
+ }
+ return auditUpstreamCollection{}, &auditHTTPError{Status: status, Code: "audit_upstream_denied", Message: "Audit source denied"}
+ }
+ var result struct {
+ ResourceType string `json:"resourceType"`
+ Data []map[string]any `json:"data"`
+ NextMarker *string `json:"nextMarker"`
+ SourcePageResult bool `json:"sourcePageResult"`
+ Authority struct {
+ ActorID string `json:"actorId"`
+ AccountID string `json:"accountId"`
+ CredentialID string `json:"credentialId"`
+ Revision int64 `json:"credentialRevision"`
+ Kind string `json:"credentialKind"`
+ } `json:"nativeAuthority"`
+ }
+ decoder := json.NewDecoder(io.LimitReader(response.Body, 4*1024*1024+1))
+ decoder.UseNumber()
+ if decoder.Decode(&result) != nil || decoder.Decode(&struct{}{}) != io.EOF || !result.SourcePageResult || len(result.Data) > 1000 ||
+ result.ResourceType != firstNonEmpty(keyKind, category) || result.Authority.ActorID != state.input.ActorID ||
+ !keyAuditIDPattern.MatchString(result.Authority.AccountID) ||
+ result.Authority.AccountID != firstNonEmpty(projectID, values.Get("projectId"), state.input.ActorID) ||
+ state.input.Authorization != "" && (result.Authority.CredentialID != state.input.CredentialID ||
+ result.Authority.Revision != state.input.Revision || result.Authority.Kind != state.input.CredentialKind) ||
+ state.input.Authorization == "" && (result.Authority.CredentialID != "" || result.Authority.Revision != 0 || result.Authority.Kind != "") {
+ return auditUpstreamCollection{}, &auditHTTPError{Status: 502, Code: "invalid_key_authority", Message: "Audit source authority changed"}
+ }
+ collection := auditUpstreamCollection{Data: result.Data, Pagination: map[string]any{}, viewerAccountID: result.Authority.ActorID}
+ if result.NextMarker != nil {
+ if len(result.Data) == 0 || !keyAuditIDPattern.MatchString(*result.NextMarker) ||
+ *result.NextMarker != auditString(result.Data[len(result.Data)-1], "id") || *result.NextMarker == values.Get("marker") {
+ return auditUpstreamCollection{}, &auditHTTPError{Status: 502, Code: "invalid_audit_response", Message: "Audit source pagination rejected"}
+ }
+ next := url.Values{}
+ for name, items := range values {
+ next[name] = append([]string(nil), items...)
+ }
+ next.Set("marker", *result.NextMarker)
+ collection.Pagination["next"] = "/v2-beta/auditlogs?" + next.Encode()
+ }
+ return collection, nil
+}
+
+// Public route forwards one fixed typed Engine request. The Engine then invokes
+// the private CLI and records the actual whole-query outcome in its durable sink.
+func (b *broker) serveAIAuditGateway(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ w.Header().Set("Allow", "POST")
+ writeJSONError(w, 405, "method_not_allowed", "Method not allowed")
+ return
+ }
+ if r.URL.RawQuery != "" {
+ writeAuditFailure(w, aiAuditInvalid())
+ return
+ }
+ body, err := io.ReadAll(io.LimitReader(r.Body, 8193))
+ if err != nil || len(body) > 8192 {
+ writeAuditFailure(w, aiAuditInvalid())
+ return
+ }
+ target := *b.upstreamURL
+ target.Path = "/v2-beta/aiAuditQueries"
+ target.RawPath = ""
+ target.RawQuery = ""
+ target.Fragment = ""
+ request, err := http.NewRequestWithContext(r.Context(), "POST", target.String(), bytes.NewReader(body))
+ if err != nil {
+ writeJSONError(w, 502, "audit_query_failed", "Audit query unavailable")
+ return
+ }
+ for _, header := range []string{"Cookie", "Authorization", "X-PastureStack-Key-Expected-Revision"} {
+ if value := r.Header.Get(header); value != "" {
+ request.Header.Set(header, value)
+ }
+ }
+ request.Header.Set("Content-Type", "application/json")
+ client := &http.Client{Timeout: 50 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("audit gateway redirect denied") }}
+ response, err := client.Do(request)
+ if err != nil {
+ writeJSONError(w, 502, "audit_query_failed", "Audit query unavailable")
+ return
+ }
+ defer response.Body.Close()
+ result, err := io.ReadAll(io.LimitReader(response.Body, 512*1024+1))
+ if err != nil || len(result) > 512*1024 {
+ writeJSONError(w, 502, "audit_query_failed", "Audit query unavailable")
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "no-store")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+ for _, header := range []string{"X-PastureStack-Key-Purpose", "X-PastureStack-Key-Revision"} {
+ if value := response.Header.Get(header); value != "" {
+ w.Header().Set(header, value)
+ }
+ }
+ w.WriteHeader(response.StatusCode)
+ _, _ = w.Write(result)
+}
+
+func aiAuditCLI() bool {
+ if len(os.Args) == 2 && os.Args[1] == "--ai-audit-query" {
+ os.Exit(runAIAuditOwnerCLI(os.Stdin, os.Stdout))
+ return true
+ }
+ return false
+}
diff --git a/server/console-broker/ai_audit_owner_test.go b/server/console-broker/ai_audit_owner_test.go
new file mode 100644
index 0000000000..6d8f9ee25a
--- /dev/null
+++ b/server/console-broker/ai_audit_owner_test.go
@@ -0,0 +1,224 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+)
+
+func ownerTestBroker(t *testing.T, handler http.Handler) *broker {
+ t.Helper()
+ upstream := httptest.NewServer(handler)
+ t.Cleanup(upstream.Close)
+ b, err := newBroker(brokerConfig{UpstreamURL: upstream.URL, SessionDialURL: upstream.URL, MaxSessions: 1, ReplayBytes: 65536,
+ ActiveTTL: time.Hour, HistoryTTL: time.Hour, CleanupInterval: time.Hour}, log.New(io.Discard, "", 0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(b.close)
+ return b
+}
+
+func ownerInput() aiAuditOwnerInput {
+ return aiAuditOwnerInput{Query: &aiAuditInput{From: "2026-10-01T00:00:00Z", To: "2026-10-02T00:00:00Z", EnvironmentID: "1a5"},
+ ActorID: "1a1", Authorization: "Basic PRIVATE_VERIFIED_CREDENTIAL", CredentialID: "1c8", CredentialKind: "apiKey", Revision: 3}
+}
+
+func ownerFrame(t *testing.T, b *broker, input aiAuditOwnerInput) (int, map[string]any, string) {
+ t.Helper()
+ var output bytes.Buffer
+ if executeAIAuditOwner(b, input, &output) != 0 {
+ t.Fatal("owner produced no complete frame")
+ }
+ var result struct {
+ Status int
+ Body map[string]any
+ }
+ if json.Unmarshal(output.Bytes(), &result) != nil {
+ t.Fatal("owner frame invalid")
+ }
+ return result.Status, result.Body, output.String()
+}
+
+func sourceFixture(t *testing.T, mutate func(string, map[string]any, map[string]any) int) http.Handler {
+ t.Helper()
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" || r.URL.Path != "/v2-beta/aiResourceViews" || r.URL.RawQuery != "" ||
+ r.Header.Get("Authorization") != "Basic PRIVATE_VERIFIED_CREDENTIAL" ||
+ r.Header.Get("X-PastureStack-Key-Expected-Revision") != "3" ||
+ r.Header.Get("Cookie") != "" || r.Header.Get("X-Forwarded-User") != "" {
+ t.Error("source escaped exact private caller binding")
+ w.WriteHeader(500)
+ return
+ }
+ var input map[string]any
+ if json.NewDecoder(r.Body).Decode(&input) != nil {
+ t.Fatal("source input invalid")
+ }
+ category := auditString(input, "resourceType")
+ page := input["sourcePage"].(map[string]any)
+ project := auditString(page, "projectId")
+ authority := map[string]any{"actorId": "1a1", "accountId": firstNonEmpty(project, "1a1"), "credentialId": "1c8", "credentialKind": "apiKey", "credentialRevision": 3}
+ rows := []map[string]any{}
+ switch category {
+ case "project":
+ rows = append(rows, map[string]any{"id": "1a5", "type": "project", "name": "Visible"})
+ case "account":
+ rows = append(rows, map[string]any{"id": "1a1", "type": "account", "name": "Viewer"})
+ case "apiKey":
+ if project == "" {
+ w.WriteHeader(403)
+ return
+ }
+ rows = append(rows, map[string]any{"id": "1c9", "type": page["keyKind"], "accountId": "1a5"})
+ case "auditLog":
+ if project != "" {
+ rows = append(rows, map[string]any{"id": "1al10", "type": "auditLog", "accountId": "1a5", "authenticatedAsAccountId": "1a1",
+ "keyId": "1c9", "created": "2026-10-01T12:00:00Z", "eventType": "api.service.update", "decision": "ALLOW", "outcome": "SUCCEEDED"})
+ }
+ default:
+ t.Error("unexpected native source")
+ w.WriteHeader(500)
+ return
+ }
+ result := map[string]any{"resourceType": firstNonEmpty(auditString(page, "keyKind"), category), "data": rows, "nextMarker": nil,
+ "nativeAuthority": authority, "sourcePageResult": true}
+ if mutate != nil {
+ if status := mutate(category, page, result); status != 0 {
+ w.WriteHeader(status)
+ return
+ }
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(result)
+ })
+}
+
+func TestAIAuditPrivateOwnerUsesFourNativeSourcesAndOriginalKeyAuthority(t *testing.T) {
+ seen := map[string]int{}
+ b := ownerTestBroker(t, sourceFixture(t, func(category string, page, result map[string]any) int { seen[category]++; return 0 }))
+ input := ownerInput()
+ input.Query.KeyID = "1c9"
+ status, body, raw := ownerFrame(t, b, input)
+ if status != 200 || body["total"] != float64(1) || strings.Contains(raw, "PRIVATE") || body["payloadPolicy"] != "HUMAN_ONLY_NATIVE_AUDIT_UI" {
+ t.Fatal("bounded original aggregation or private boundary failed")
+ }
+ for _, category := range []string{"project", "account", "apiKey", "auditLog"} {
+ if seen[category] == 0 {
+ t.Fatal("source not checked:", category)
+ }
+ }
+ row := body["data"].([]any)[0].(map[string]any)
+ if row["keyId"] != "1c9" || row["payloadsIncluded"] != false || row["requestPayloadPresence"] != "not_projected" {
+ t.Fatal("key query projection changed")
+ }
+}
+
+func TestAIAuditPrivateSourcesFailClosedOnEveryAuthorityDriftOrDeniedPage(t *testing.T) {
+ for _, category := range []string{"project", "account", "apiKey", "auditLog"} {
+ for _, failure := range []string{"deny", "actor", "revision", "credential", "crossProject"} {
+ t.Run(category+"/"+failure, func(t *testing.T) {
+ b := ownerTestBroker(t, sourceFixture(t, func(actual string, page, result map[string]any) int {
+ if actual != category {
+ return 0
+ }
+ authority := result["nativeAuthority"].(map[string]any)
+ switch failure {
+ case "deny":
+ return 403
+ case "actor":
+ authority["actorId"] = "1a2"
+ case "revision":
+ authority["credentialRevision"] = 4
+ case "credential":
+ authority["credentialId"] = "1c90"
+ case "crossProject":
+ authority["accountId"] = "1a99"
+ }
+ return 0
+ }))
+ input := ownerInput()
+ input.Query.KeyID = "1c9"
+ status, body, raw := ownerFrame(t, b, input)
+ // Account names are optional in the original owner, but an
+ // identity/revision mismatch is never an optional 403.
+ if category == "account" && failure == "deny" {
+ if status != 200 {
+ t.Fatal("original optional account-name behavior changed")
+ }
+ return
+ }
+ if status == 200 || body["data"] != nil || strings.Contains(raw, "PRIVATE") {
+ t.Fatal("partial aggregate escaped source failure")
+ }
+ })
+ }
+ }
+}
+
+func TestAIAuditPrivatePaginationRechecksRevisionAndNeverReturnsPartialRows(t *testing.T) {
+ calls := 0
+ b := ownerTestBroker(t, sourceFixture(t, func(category string, page, result map[string]any) int {
+ if category != "auditLog" || page["projectId"] == nil {
+ return 0
+ }
+ calls++
+ if calls == 1 {
+ result["nextMarker"] = "1al10"
+ } else {
+ result["nativeAuthority"].(map[string]any)["credentialRevision"] = 4
+ }
+ return 0
+ }))
+ status, body, _ := ownerFrame(t, b, ownerInput())
+ if status == 200 || body["data"] != nil || calls != 2 {
+ t.Fatal("later revision returned partial success")
+ }
+}
+
+func TestAIAuditPublicGatewayIsFixedTypedAndForwardsOnlyVerifiedResponseMetadata(t *testing.T) {
+ calls := 0
+ b := ownerTestBroker(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ calls++
+ if r.Method != "POST" || r.URL.Path != "/v2-beta/aiAuditQueries" || r.URL.RawQuery != "" ||
+ r.Header.Get("Authorization") != "Basic caller" || r.Header.Get("X-PastureStack-Key-Expected-Revision") != "3" ||
+ r.Header.Get("X-Forwarded-User") != "" || r.Header.Get("X-API-Project-ID") != "" {
+ t.Error("gateway escaped fixed route/authority")
+ }
+ w.Header().Set("X-PastureStack-Key-Purpose", "ai-bridge")
+ w.Header().Set("X-PastureStack-Key-Revision", "3")
+ _, _ = w.Write([]byte(`{"resourceType":"auditLog","data":[]}`))
+ }))
+ r := httptest.NewRequest("POST", aiAuditQueryPath, strings.NewReader("{}"))
+ r.Header.Set("Authorization", "Basic caller")
+ r.Header.Set("X-PastureStack-Key-Expected-Revision", "3")
+ r.Header.Set("X-Forwarded-User", "forged")
+ r.Header.Set("X-API-Project-ID", "foreign")
+ w := httptest.NewRecorder()
+ b.ServeHTTP(w, r)
+ if w.Code != 200 || calls != 1 || w.Header().Get("X-PastureStack-Key-Revision") != "3" {
+ t.Fatal("fixed typed gateway failed")
+ }
+ r = httptest.NewRequest("POST", aiAuditQueryPath+"?url=http://foreign/", strings.NewReader("{}"))
+ w = httptest.NewRecorder()
+ b.ServeHTTP(w, r)
+ if w.Code != 400 || calls != 1 {
+ t.Fatal("arbitrary query forwarded")
+ }
+}
+
+func TestAIAuditCLIRejectsUnverifiedUnknownOrTruncatedPrivateInputWithoutOutput(t *testing.T) {
+ for _, input := range []string{`{}`, `{"query":{},"actorId":"1a1","url":"http://foreign/","cookie":"secret"}`,
+ `{"query":{},"actorId":"1a1","cookie":"a","authorization":"b"}`, `{"query":{}`, strings.Repeat("x", 65538)} {
+ var output bytes.Buffer
+ if runAIAuditOwnerCLI(strings.NewReader(input), &output) == 0 || output.Len() != 0 {
+ t.Fatal("invalid private input was accepted or disclosed")
+ }
+ }
+}
diff --git a/server/console-broker/ai_audit_test.go b/server/console-broker/ai_audit_test.go
new file mode 100644
index 0000000000..90c4ac91de
--- /dev/null
+++ b/server/console-broker/ai_audit_test.go
@@ -0,0 +1,236 @@
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+const aiAuditRange = `"from":"2026-10-01T00:00:00Z","to":"2026-10-02T00:00:00Z"`
+
+// These tests call the isolated query owner. Public fixed forwarding has a separate gateway test.
+func postAIAudit(t *testing.T, server *httptest.Server, payload, cookie, authorization string) (int, map[string]any, string) {
+ t.Helper()
+ request, err := http.NewRequest(http.MethodPost, server.URL+aiAuditQueryPath, strings.NewReader(payload))
+ if err != nil {
+ t.Fatal(err)
+ }
+ request.Header.Set("Content-Type", "application/json")
+ if cookie != "" {
+ request.Header.Set("Cookie", cookie)
+ }
+ if authorization != "" {
+ request.Header.Set("Authorization", authorization)
+ }
+ response, err := server.Client().Do(request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer response.Body.Close()
+ raw, err := io.ReadAll(io.LimitReader(response.Body, 65536))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var body map[string]any
+ if err := json.Unmarshal(raw, &body); err != nil {
+ t.Fatal("typed response was not JSON")
+ }
+ return response.StatusCode, body, string(raw)
+}
+
+func TestAIAuditNativeOwnerQueryPaginationAndSafeDetail(t *testing.T) {
+ upstream := newAuditUpstreamFixture(t)
+ for _, id := range []string{"audit1", "audit2"} {
+ row := auditTestRecord(id, "2026-10-01T12:00:00Z", "1p1", "1a1", "api.service.update", "TokenAuth", "PRIVATE_DESCRIPTION")
+ row["decision"], row["outcome"], row["httpStatus"] = "DENY", "FAILED", 403
+ row["responseObject"] = map[string]any{"jwt": "PRIVATE_RESPONSE"}
+ upstream.auditRecords = append(upstream.auditRecords, row)
+ }
+ upstream.auditRecords = append(upstream.auditRecords,
+ auditTestRecord("foreign", "2026-10-01T12:00:00Z", "foreign-project", "1a1", "event", "TokenAuth", "PRIVATE_FOREIGN"))
+ server := newAuditTestBroker(t, upstream, true)
+ payload := `{` + aiAuditRange + `,"environmentId":"1p1","decision":"DENY","limit":1}`
+ status, body, raw := postAIAudit(t, server, payload, "R_SESS=authorized", "")
+ if status != 200 || body["total"] != float64(2) || body["nextOffset"] != float64(1) {
+ t.Fatal("native authorized query/page did not match")
+ }
+ if strings.Contains(raw, "PRIVATE_") || strings.Contains(raw, "must-not-export") || strings.Contains(raw, "requestObject") {
+ t.Fatal("private native payload reached typed metadata")
+ }
+ first := body["data"].([]any)[0].(map[string]any)
+ if first["payloadsIncluded"] != false || first["responsePayloadPresence"] != "present" || first["httpStatus"] != float64(403) {
+ t.Fatal("safe payload policy or HTTP metadata missing")
+ }
+ status, second, _ := postAIAudit(t, server, `{`+aiAuditRange+`,"environmentId":"1p1","decision":"DENY","limit":1,"offset":1}`, "R_SESS=authorized", "")
+ if status != 200 || second["nextOffset"] != nil || second["data"].([]any)[0].(map[string]any)["id"] == first["id"] {
+ t.Fatal("native second page was not distinct and terminal")
+ }
+ status, detail, _ := postAIAudit(t, server, `{`+aiAuditRange+`,"environmentId":"1p1","eventId":"`+first["id"].(string)+`"}`, "R_SESS=authorized", "")
+ if status != 200 || len(detail["data"].([]any)) != 1 {
+ t.Fatal("exact authorized event detail failed")
+ }
+ status, _, _ = postAIAudit(t, server, `{`+aiAuditRange+`,"environmentId":"1p1","eventId":"foreign"}`, "R_SESS=authorized", "")
+ if status != 404 {
+ t.Fatal("foreign event escaped original owner query scope")
+ }
+}
+
+func TestAIAuditKeyFilterReusesLiveAuthorityAndCannotReadAnotherKey(t *testing.T) {
+ fixture, server := newKeyAuditFixture(t, true)
+ fixture.keyID = "1c99"
+ fixture.records = []map[string]any{
+ keyAuditRecord("own-event", "1c99", "1p1", "2026-10-01T12:00:00Z"),
+ keyAuditRecord("other-event", "1c100", "1p1", "2026-10-01T12:00:00Z"),
+ }
+ payload := `{` + aiAuditRange + `,"keyId":"1c99","environmentId":"1p1","decision":"ALLOW"}`
+ status, body, raw := postAIAudit(t, server, payload, "R_SESS=authorized", "")
+ if status != 200 || len(body["data"].([]any)) != 1 || len(fixture.keyQueries) == 0 || strings.Contains(raw, "secret") {
+ t.Fatal("exact Key audit owner/projection failed")
+ }
+ status, _, _ = postAIAudit(t, server, `{`+aiAuditRange+`,"keyId":"1c99","eventId":"other-event"}`, "R_SESS=authorized", "")
+ if status != 404 {
+ t.Fatal("cross-Key detail was not refused")
+ }
+ fixture.keyStatus = http.StatusForbidden
+ status, _, _ = postAIAudit(t, server, payload, "R_SESS=authorized", "")
+ if status != 403 {
+ t.Fatal("Key visibility loss was not rechecked")
+ }
+}
+
+func TestAIAuditStrictInputsAndNativeCookieBoundary(t *testing.T) {
+ upstream := newAuditUpstreamFixture(t)
+ server := newAuditTestBroker(t, upstream, true)
+ for _, payload := range []string{
+ `null`, `{"url":"https://outside.invalid"}`, `{"actorId":"1a1"}`,
+ `{"limit":51}`, `{"offset":20000}`, `{"lastMinutes":0}`,
+ `{"lastMinutes":1,"from":"2026-10-01T00:00:00Z"}`,
+ `{"from":"2026-10-01T00:00:00"}`, `{"decision":"ALLOW OR DENY"}`,
+ `{} {}`, `{"requestId":"` + strings.Repeat("x", 9000) + `"}`,
+ } {
+ status, _, _ := postAIAudit(t, server, payload, "R_SESS=authorized", "")
+ if status != 400 && status != 422 {
+ t.Fatal("invalid typed input was accepted")
+ }
+ }
+ for _, credentials := range [][2]string{{"", ""}, {"", "Basic not-a-cookie"}, {"R_SESS=authorized", "Basic not-a-cookie"}} {
+ status, body, _ := postAIAudit(t, server, "{}", credentials[0], credentials[1])
+ if status != 403 || body["code"] != "ai_audit_cookie_required" {
+ t.Fatal("unsupported transport did not fail closed")
+ }
+ }
+ if len(upstream.auditQueries) != 0 {
+ t.Fatal("rejected inputs/transport reached audit data")
+ }
+}
+
+func TestAIAuditSessionAndEnvironmentLossNeverReturnPartialRows(t *testing.T) {
+ upstream := newAuditUpstreamFixture(t)
+ server := newAuditTestBroker(t, upstream, true)
+ for _, test := range []struct {
+ cookie, payload string
+ status int
+ }{
+ {"R_SESS=expired", "{}", 401},
+ {"R_SESS=authorized", `{"environmentId":"unavailable"}`, 403},
+ } {
+ status, body, _ := postAIAudit(t, server, test.payload, test.cookie, "")
+ if status != test.status || body["data"] != nil {
+ t.Fatal("current authorization rejection returned success/partial data")
+ }
+ }
+ if len(upstream.auditQueries) != 0 {
+ t.Fatal("failed authority read reached audit data")
+ }
+}
+
+func TestAIAuditRejectsRedirectBeforeAnyForeignSourceCall(t *testing.T) {
+ hits := 0
+ foreign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ hits++
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer foreign.Close()
+ upstream := &auditUpstreamFixture{server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, foreign.URL, http.StatusFound)
+ }))}
+ defer upstream.server.Close()
+ server := newAuditTestBroker(t, upstream, true)
+ status, _, _ := postAIAudit(t, server, "{}", "R_SESS=authorized", "")
+ if status != 502 || hits != 0 {
+ t.Fatal("typed native audit followed an authority redirect")
+ }
+}
+
+func TestAIAuditLegacyHumanAuditPayloadContractIsUnchanged(t *testing.T) {
+ upstream := newAuditUpstreamFixture(t)
+ upstream.auditRecords = []map[string]any{
+ auditTestRecord("legacy", "2026-10-01T12:00:00Z", "1p1", "1a1", "event", "TokenAuth", "human description"),
+ }
+ server := newAuditTestBroker(t, upstream, true)
+ response := performAuditRequest(t, server, auditQueryPath+"?createdFrom=2026-10-01T00:00:00Z&createdTo=2026-10-02T00:00:00Z")
+ defer response.Body.Close()
+ body, _ := io.ReadAll(response.Body)
+ if response.StatusCode != 200 || !strings.Contains(string(body), "requestObject") {
+ t.Fatal("ordinary human audit detail was changed by the typed adapter")
+ }
+}
+
+func TestAIAuditOriginalOwnerScanLimitReturnsNoPartialProjection(t *testing.T) {
+ upstream := newAuditUpstreamFixture(t)
+ record := map[string]any{"id": "bounded", "accountId": "1p1", "created": "2026-10-01T12:00:00Z"}
+ upstream.auditRecords = make([]map[string]any, auditMaximumScanRows+1)
+ for index := range upstream.auditRecords {
+ upstream.auditRecords[index] = record
+ }
+ server := newAuditTestBroker(t, upstream, true)
+ status, body, _ := postAIAudit(t, server, `{`+aiAuditRange+`}`, "R_SESS=authorized", "")
+ if status != 422 || body["code"] != "result_set_too_large" || body["data"] != nil {
+ t.Fatal("original owner's scan bound was bypassed or returned partial success")
+ }
+}
+
+func TestAIAuditCallerHeadersCannotSupplyOwnerIdentityOrProject(t *testing.T) {
+ calls := 0
+ upstream := &auditUpstreamFixture{server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ calls++
+ if r.Header.Get("Cookie") != "R_SESS=authorized" || r.Header.Get("X-Forwarded-User") != "" ||
+ r.Header.Get("X-Api-Auth-Header") != "" || r.Header.Get("X-API-Project-ID") != "" {
+ t.Error("new route forwarded caller-selected authority")
+ w.WriteHeader(500)
+ return
+ }
+ w.Header().Set("X-API-USER-ID", "1a1")
+ switch r.URL.Path {
+ case "/v2-beta/projects":
+ writeFixtureCollection(w, []map[string]any{{"id": "1p1", "type": "project"}}, "")
+ case "/v2-beta/accounts":
+ writeFixtureCollection(w, nil, "")
+ case "/v2-beta/auditlogs":
+ writeFixtureCollection(w, []map[string]any{
+ auditTestRecord("event", "2026-10-01T12:00:00Z", "1p1", "1a1", "event", "TokenAuth", "private"),
+ }, "")
+ default:
+ t.Error("unexpected native owner route")
+ w.WriteHeader(500)
+ }
+ }))}
+ defer upstream.server.Close()
+ server := newAuditTestBroker(t, upstream, true)
+ request, _ := http.NewRequest("POST", server.URL+aiAuditQueryPath, strings.NewReader(`{`+aiAuditRange+`,"environmentId":"1p1"}`))
+ request.Header.Set("Cookie", "R_SESS=authorized")
+ request.Header.Set("X-Forwarded-User", "forged-actor")
+ request.Header.Set("X-Api-Auth-Header", "forged-identity")
+ request.Header.Set("X-API-Project-ID", "foreign-project")
+ response, err := server.Client().Do(request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer response.Body.Close()
+ if response.StatusCode != 200 || calls != 3 {
+ t.Fatal("native owner identity was not kept independent of caller headers")
+ }
+}
diff --git a/server/console-broker/audit.go b/server/console-broker/audit.go
index cf90cf0f0f..683009dedf 100644
--- a/server/console-broker/audit.go
+++ b/server/console-broker/audit.go
@@ -265,7 +265,7 @@ func (b *broker) fetchAuditProjects(ctx context.Context, incoming *http.Request,
if err != nil {
return nil, "", err
}
- if keyQuery {
+ if keyQuery || auditOwner(ctx) != nil {
if len(projectPage.Data) > auditUpstreamPageSize {
return nil, "", &auditHTTPError{Status: http.StatusUnprocessableEntity, Code: "result_set_too_large", Message: "Current environment permissions exceeded the bounded authority query"}
}
@@ -366,6 +366,9 @@ func (b *broker) runAuditQuery(ctx context.Context, incoming *http.Request, quer
// memberships at once. The Key view must query each live authorized
// environment explicitly rather than silently showing personal rows only.
records, err = b.fetchKeyAuditLogs(ctx, incoming, query, allowedProjects, upstreamValues)
+ } else if auditOwner(ctx) != nil {
+ query.ViewerAccountID = viewer
+ records, err = b.fetchKeyAuditLogs(ctx, incoming, query, allowedProjects, upstreamValues)
} else {
records, err = b.fetchAllAuditLogs(ctx, incoming, upstreamValues)
}
@@ -494,6 +497,13 @@ func (b *broker) fetchAuditPage(ctx context.Context, incoming *http.Request, pat
}
func (b *broker) fetchAuditPagePolicy(ctx context.Context, incoming *http.Request, path string, values url.Values, keyQuery bool) (auditUpstreamCollection, error) {
+ if auditOwner(ctx) != nil {
+ category := map[string]string{"/v2-beta/projects": "project", "/v2-beta/accounts": "account", "/v2-beta/auditlogs": "auditLog"}[path]
+ if category == "" {
+ return auditUpstreamCollection{}, aiAuditInvalid()
+ }
+ return b.fetchAIAuditSource(ctx, incoming, category, "", "", "", values)
+ }
target := *b.upstreamURL
target.Path = path
target.RawPath = ""
@@ -508,7 +518,7 @@ func (b *broker) fetchAuditPagePolicy(ctx context.Context, incoming *http.Reques
request.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 20 * time.Second}
- if keyQuery {
+ if keyQuery || isAIAuditRead(ctx) {
client.CheckRedirect = func(*http.Request, []*http.Request) error {
return errors.New("API key audit redirects are not allowed")
}
diff --git a/server/console-broker/audit_test.go b/server/console-broker/audit_test.go
index ad74d5bd1c..4fa0ad4cda 100644
--- a/server/console-broker/audit_test.go
+++ b/server/console-broker/audit_test.go
@@ -126,7 +126,7 @@ func writeFixtureCollection(writer http.ResponseWriter, records []map[string]any
})
}
-func newAuditTestBroker(t *testing.T, fixture *auditUpstreamFixture) *httptest.Server {
+func newAuditTestBroker(t *testing.T, fixture *auditUpstreamFixture, ownerOnly ...bool) *httptest.Server {
t.Helper()
cfg := brokerConfig{
ListenAddress: ":0", UpstreamURL: fixture.server.URL, SessionDialURL: fixture.server.URL,
@@ -136,7 +136,13 @@ func newAuditTestBroker(t *testing.T, fixture *auditUpstreamFixture) *httptest.S
if err != nil {
t.Fatal(err)
}
- server := httptest.NewServer(instance)
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if len(ownerOnly) > 0 && ownerOnly[0] && r.URL.Path == aiAuditQueryPath {
+ instance.serveAIAudit(w, r)
+ return
+ }
+ instance.ServeHTTP(w, r)
+ }))
t.Cleanup(func() {
server.Close()
instance.close()
diff --git a/server/console-broker/broker.go b/server/console-broker/broker.go
index 2d1a2a59af..20c3e4df8b 100644
--- a/server/console-broker/broker.go
+++ b/server/console-broker/broker.go
@@ -254,6 +254,11 @@ func (b *broker) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return
}
+ if request.URL.Path == aiAuditQueryPath {
+ b.serveAIAuditGateway(writer, request)
+ return
+ }
+
if isKeyAuditPath(request.URL.Path) {
b.serveKeyAudit(writer, request)
return
diff --git a/server/console-broker/keyaudit.go b/server/console-broker/keyaudit.go
index 27e78e3c90..3fa29bf198 100644
--- a/server/console-broker/keyaudit.go
+++ b/server/console-broker/keyaudit.go
@@ -191,6 +191,26 @@ func (b *broker) fetchKeyAuditAuthorityInProject(ctx context.Context, incoming *
return errors.New("API key authority redirects are not allowed")
}}
for index, kind := range []string{"apiKey", "apiKeyRestricted"} {
+ if auditOwner(ctx) != nil {
+ collection, err := b.fetchAIAuditSource(ctx, incoming, "apiKey", keyID, projectID, kind, url.Values{})
+ if err != nil {
+ var denied *auditHTTPError
+ if errors.As(err, &denied) && denied.Status == 404 && index == 0 {
+ continue
+ }
+ if errors.As(err, &denied) && (denied.Status == 403 || denied.Status == 404) {
+ return "", "", &auditHTTPError{Status: 403, Code: "key_audit_access_lost", Message: "API key unavailable"}
+ }
+ return "", "", err
+ }
+ if len(collection.Data) != 1 || auditString(collection.Data[0], "id") != keyID ||
+ !strings.EqualFold(auditString(collection.Data[0], "type"), kind) ||
+ !keyAuditIDPattern.MatchString(auditString(collection.Data[0], "accountId")) ||
+ projectID != "" && (auditString(collection.Data[0], "accountId") != projectID || collection.viewerAccountID != principal) {
+ return "", "", &auditHTTPError{Status: 502, Code: "invalid_key_authority", Message: "API key authority invalid"}
+ }
+ return auditString(collection.Data[0], "accountId"), collection.viewerAccountID, nil
+ }
target.Path, target.RawPath, target.RawQuery, target.Fragment = "/v2-beta/"+strings.ToLower(kind)+"/"+keyID, "", "", ""
if projectID != "" {
target.RawQuery = url.Values{"projectId": {projectID}}.Encode()
diff --git a/server/console-broker/keyaudit_test.go b/server/console-broker/keyaudit_test.go
index 723564818d..c089393ff1 100644
--- a/server/console-broker/keyaudit_test.go
+++ b/server/console-broker/keyaudit_test.go
@@ -42,7 +42,7 @@ type keyAuditFixture struct {
redirectHits int
}
-func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) {
+func newKeyAuditFixture(t *testing.T, ownerOnly ...bool) (*keyAuditFixture, *httptest.Server) {
t.Helper()
fixture := &keyAuditFixture{
keyID: "1a99", owner: "1a1", viewer: "1a1", keyStatus: http.StatusOK,
@@ -169,7 +169,7 @@ func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) {
}
}))
t.Cleanup(upstream.server.Close)
- return fixture, newAuditTestBroker(t, upstream)
+ return fixture, newAuditTestBroker(t, upstream, ownerOnly...)
}
func keyAuditRecord(id, keyID, project, created string) map[string]any {
diff --git a/server/console-broker/main.go b/server/console-broker/main.go
index 44b4599aa8..ab7801790f 100644
--- a/server/console-broker/main.go
+++ b/server/console-broker/main.go
@@ -13,6 +13,9 @@ import (
)
func main() {
+ if aiAuditCLI() {
+ return
+ }
cfg, err := configFromEnvironment()
if err != nil {
log.Fatalf("invalid console broker configuration: %s", safeLogValue(err))
diff --git a/server/security/openvex.json b/server/security/openvex.json
index 47e85a8895..f9660d8d1f 100644
--- a/server/security/openvex.json
+++ b/server/security/openvex.json
@@ -1,9 +1,9 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
- "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.559",
+ "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.560",
"author": "PastureStack Security",
- "timestamp": "2026-10-29T23:21:31Z",
- "version": 72,
+ "timestamp": "2026-10-30T01:53:41Z",
+ "version": 73,
"statements": [
{
"vulnerability": {
diff --git a/server/security/vendor-pending.json b/server/security/vendor-pending.json
index 3c5d6e368c..1b9cfb27cd 100644
--- a/server/security/vendor-pending.json
+++ b/server/security/vendor-pending.json
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
- "release": "v1.6.559",
+ "release": "v1.6.560",
"generatedAt": "2026-10-29T09:57:33.809116+00:00",
"policy": {
"allowedSeverities": [