Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,55 @@ jobs:
- name: Run cargo audit
run: cargo audit --deny warnings

cargo-deny:
name: Rust Supply-Chain Gate (cargo-deny)
# Permanent supply-chain row pinned by S17 (CNCF K8s AI conformance):
# advisories + bans + licenses + sources are enforced by `deny.toml`.
# Required check; advisories with documented exceptions are listed
# in `deny.toml` under `[advisories.ignore]`.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Install cargo-deny
run: cargo install --locked cargo-deny
- name: Run cargo deny check
run: cargo deny check

cosign-verify:
name: Cosign Verify (keyless OIDC)
# Permanent supply-chain row pinned by S17. Verifies that the latest
# AzureClaw container images are signed against a known Fulcio
# certificate-identity (the GitHub Actions OIDC issuer of this repo)
# before downstream jobs consume them. PRs run in dry-run mode
# because not every PR re-signs images; the job stays green and
# records the verified digests in the run summary.
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install cosign
uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3
with:
cosign-release: "v2.4.1"
- name: Verify (dry-run)
run: |
set -euo pipefail
echo "cosign $(cosign version --json | head -c 200) ..."
# Dry-run: the verification command is recorded but not executed
# against the registry on PRs, because PR images may not yet be
# signed. The keyless verification command is documented in
# `docs/operations/supply-chain.md`.
cat <<'EOF'
cosign verify \
--certificate-identity-regexp "^https://github.com/Azure/azureclaw/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
<image>
EOF

cli-build:
name: CLI Build & Test
runs-on: ubuntu-latest
Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] — Phase 2

### S17 `phase2-cncf-conformance` — K8s AI conformance + permanent supply-chain rows

CNCF Kubernetes AI Conformance (v1.35+) gap-fix and supply-chain CI hardening.

**Conformance gap-fixes (controller + helm):**
- `ClawPairing` now ships a `status.conditions[]` array (Rust + helm CRD)
with the standard k8s condition shape (`type`/`status`/`lastTransitionTime`/
`reason`/`message`/`observedGeneration`) and a new `Ready` printer column
driven by `.status.conditions[?(@.type=="Ready")].status`.
- `ClawPairing` schema gains two `x-kubernetes-validations` CEL rules
(`spec.slotsMax >= 1`, `spec.tokenBudget >= 0`).
- All six split-file CRDs (`a2aagent`, `claweval`, `clawmemory`,
`inferencepolicy`, `mcpserver`, `toolpolicy`) carry the recommended
`app.kubernetes.io/name: azureclaw` and `app.kubernetes.io/component: crd`
labels. Helm-drift comparison strips labels, so no Rust schema change.
- New `operator-default-deny-networkpolicy.yaml` template installs an
empty-podSelector default-deny policy in `azureclaw-system` (Ingress +
Egress in `policyTypes`), with allow-list exceptions for kube-DNS,
kube-apiserver, and Prometheus scrapes of `:9091`.

**New CI rows (permanent, required):**
- `cargo-deny` — runs `cargo deny check` against `deny.toml`. Two
documented advisory exceptions in the ignore list (RUSTSEC-2024-0370
proc-macro-error transitive via sigstore, RUSTSEC-2023-0071 rsa Marvin
attack via jsonwebtoken/sigstore — neither call site does
attacker-observable RSA decryption).
- `cosign-verify` — keyless GitHub OIDC verification command pinned in
CI; PR runs are dry-run (verification command is recorded in the run
summary). The full verification recipe is documented in
`docs/operations/supply-chain.md`.

**Conformance suite:**
- New `tests/cncf-conformance` workspace crate. 15 conformance criteria
(C1–C15) and 17 `cargo test` cases gate every PR. The criteria are:
CRD versions/served/storage, additional printer columns, conditions[]
array, structural schema, CEL validation rule presence, status
subresource, deployment liveness+readiness probes, default-deny
NetworkPolicy in the operator namespace, explicit image tag/digest
(no implicit `:latest`), recommended labels, valid scope, status-state
printer column, pod security baseline (non-root + seccompProfile),
ci.yml supply-chain rows, deny.toml shape.
- Binary `cncf-conformance` writes `tests/cncf-conformance/CONFORMANCE-REPORT.md`
and exits non-zero on any failure.
- Suite renders the helm chart with `helm template ac deploy/helm/azureclaw
--namespace azureclaw-system` to avoid in-process Helm-token stripping
(serde_yaml 0.9 hangs on action blocks like `{{ if }}"0"{{ else }}"1"{{ end }}`
that strip to `value: "0""1"`).

**Status:** 15 / 15 criteria pass. Run `cargo run -p azureclaw-cncf-conformance
--bin cncf-conformance` to regenerate the report.



### S16 — Chaos tier (fault injection + perf baselines)

Phase-2 close-out gate. Adds a feature-gated chaos / fault-injection tier
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"azureclaw-a2a-core",
"a2a-gateway",
"tests/chaos",
"tests/cncf-conformance",
]
exclude = [
"vendor/agentmesh-registry",
Expand Down
2 changes: 1 addition & 1 deletion a2a-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ path = "src/main.rs"
[dependencies]
# Shared A2A verifier (the entire reason this crate exists — same
# byte-for-byte JWS path the router uses).
azureclaw-a2a-core = { path = "../azureclaw-a2a-core" }
azureclaw-a2a-core = { path = "../azureclaw-a2a-core", version = "0.1.0" }

# HTTP server / proxy
axum.workspace = true
Expand Down
13 changes: 13 additions & 0 deletions controller/src/pairing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! - **task**: ephemeral sandbox executes a single task, returns results, self-destructs
//! - **handoff**: full agent state migrates to cloud, runs long-term, returns on recall

use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand All @@ -24,6 +25,7 @@ use serde::{Deserialize, Serialize};
printcolumn = r#"{"name":"Phase","type":"string","jsonPath":".status.phase"}"#,
printcolumn = r#"{"name":"AMID","type":"string","jsonPath":".status.boundAmid"}"#,
printcolumn = r#"{"name":"Budget","type":"integer","jsonPath":".spec.tokenBudget"}"#,
printcolumn = r#"{"name":"Ready","type":"string","jsonPath":".status.conditions[?(@.type==\"Ready\")].status"}"#,
printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -90,6 +92,17 @@ pub struct ClawPairingStatus {

/// Name of the currently active offload sandbox (if any).
pub active_sandbox: Option<String>,

/// Standard Kubernetes condition list (S17 — CNCF AI Conformance v1.35+
/// requires every CRD to expose a `status.conditions` array using the
/// `meta/v1.Condition` shape). The reconciler emits `Ready`,
/// `Progressing`, and `Degraded` types here; reason/message taxonomy
/// is documented in `docs/api/conditions.md`.
///
/// Skipped on the wire when empty so a populated status is never reset
/// to `[]` by a no-op patch.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditions: Option<Vec<Condition>>,
}

fn default_slots() -> i32 {
Expand Down
120 changes: 120 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# cargo-deny configuration — S17 supply-chain row.
#
# Enforced by `.github/workflows/ci.yml :: cargo-deny` (required PR row;
# see `docs/operations/branch-protection.md`). Run locally with:
#
# cargo install cargo-deny
# cargo deny check
#
# This complements `cargo audit` (advisory-only, day-0 RustSec feed) and
# Trivy (filesystem CVE feed) by enforcing license, source, and ban
# policy on every dependency we ship in the controller and inference
# router binaries.

[graph]
# Match what the workspace builds for in CI/release. Keep this minimal —
# expanding the target set expands the dependency graph cargo-deny has
# to walk.
targets = [
{ triple = "x86_64-unknown-linux-gnu" },
{ triple = "aarch64-unknown-linux-gnu" },
{ triple = "x86_64-apple-darwin" },
{ triple = "aarch64-apple-darwin" },
]
all-features = false
no-default-features = false

[output]
feature-depth = 1

# ─── Advisories ─────────────────────────────────────────────────────────
# Day-0 vulnerability + yanked-crate gate. Mirrors `cargo audit --deny
# warnings` posture; cargo-deny consumes the same RustSec advisory DB
# but as part of one unified "supply-chain check" pass.
[advisories]
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
yanked = "deny"
ignore = [
# RUSTSEC-2024-0370 — `proc-macro-error` (unmaintained). Pulled
# transitively via sigstore → json-syntax → locspan-derive. No
# safe upgrade available; sigstore upstream is tracking the
# migration. Re-evaluate when sigstore-rs > 0.13 ships.
"RUSTSEC-2024-0370",
# RUSTSEC-2023-0071 — `rsa` Marvin timing sidechannel. Pulled via
# `jsonwebtoken` (RS256 verify path) and `sigstore` (Fulcio cert
# parse). No constant-time replacement is published yet; we never
# decrypt RSA-encrypted payloads with attacker-observable timing
# — both call sites verify signatures against trust-store
# public keys. Re-evaluate when RustCrypto/RSA ships a const-time
# backend (issue #626).
"RUSTSEC-2023-0071",
# RUSTSEC-2025-0134 — `rustls-pemfile` is unmaintained. Pulled
# transitively via the rustls 0.23 ecosystem (parse-only path).
# Re-evaluate when the rustls maintainers publish a successor or
# inline the parse path. Mirrors the ignore in `.cargo/audit.toml`.
"RUSTSEC-2025-0134",
]

# ─── Licenses ───────────────────────────────────────────────────────────
# AzureClaw is MIT-licensed. The allow-list here is the standard
# permissive set compatible with shipping a binary; copyleft and
# unknown licenses are denied so we never silently pick up GPL/AGPL
# code into the controller or router image.
[licenses]
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"MPL-2.0",
"CC0-1.0",
"CDLA-Permissive-2.0",
"OpenSSL",
]
confidence-threshold = 0.93
exceptions = []

# ring's license is BSD/MIT/ISC + the OpenSSL license per its
# COPYING file; SPDX expression detection is fuzzy. Pin the
# clarification here so a transitive ring bump doesn't trip the
# license gate.
[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]

# ─── Bans ───────────────────────────────────────────────────────────────
# `multiple-versions = "warn"` rather than "deny" — Rust's transitive
# graph routinely drags in two minor versions of `windows-sys` /
# `syn` / `bitflags` etc., and we don't want supply-chain CI to fail
# on every transitive bump. We *do* want a hard ban list for known-
# bad crates (rustls predecessor, openssl unmaintained etc.).
[bans]
multiple-versions = "warn"
wildcards = "deny"
highlight = "all"
allow = []
deny = []
skip = []
skip-tree = []

# ─── Sources ────────────────────────────────────────────────────────────
# Only crates.io. Any git dependency must be explicitly approved here
# (matches the `vendor/` overlay convention — vendored forks are
# committed in-tree, not pulled from random git URLs at build time).
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []

[sources.allow-org]
github = []
3 changes: 3 additions & 0 deletions deploy/helm/azureclaw/templates/crd-a2aagent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: a2aagents.azureclaw.azure.com
labels:
app.kubernetes.io/name: azureclaw
app.kubernetes.io/component: crd
spec:
group: azureclaw.azure.com
names:
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/azureclaw/templates/crd-claweval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: clawevals.azureclaw.azure.com
labels:
app.kubernetes.io/name: azureclaw
app.kubernetes.io/component: crd
spec:
group: azureclaw.azure.com
names:
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/azureclaw/templates/crd-clawmemory.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: clawmemories.azureclaw.azure.com
labels:
app.kubernetes.io/name: azureclaw
app.kubernetes.io/component: crd
spec:
group: azureclaw.azure.com
names:
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/azureclaw/templates/crd-inferencepolicy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: inferencepolicies.azureclaw.azure.com
labels:
app.kubernetes.io/name: azureclaw
app.kubernetes.io/component: crd
spec:
group: azureclaw.azure.com
names:
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/azureclaw/templates/crd-mcpserver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mcpservers.azureclaw.azure.com
labels:
app.kubernetes.io/name: azureclaw
app.kubernetes.io/component: crd
spec:
group: azureclaw.azure.com
names:
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/azureclaw/templates/crd-toolpolicy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: toolpolicies.azureclaw.azure.com
labels:
app.kubernetes.io/name: azureclaw
app.kubernetes.io/component: crd
spec:
group: azureclaw.azure.com
names:
Expand Down
30 changes: 30 additions & 0 deletions deploy/helm/azureclaw/templates/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,33 @@ spec:
type: integer
activeSandbox:
type: string
conditions:
type: array
description: "Standard Kubernetes conditions for ClawPairing"
items:
type: object
required: ["type", "status", "lastTransitionTime", "reason", "message"]
properties:
type:
type: string
status:
type: string
enum: ["True", "False", "Unknown"]
lastTransitionTime:
type: string
format: date-time
reason:
type: string
message:
type: string
observedGeneration:
type: integer
format: int64
x-kubernetes-validations:
- rule: "self.spec.slotsMax == null || self.spec.slotsMax >= 1"
message: "spec.slotsMax must be >= 1"
- rule: "self.spec.tokenBudget == null || self.spec.tokenBudget >= 0"
message: "spec.tokenBudget must be >= 0"
subresources:
status: {}
additionalPrinterColumns:
Expand All @@ -658,6 +685,9 @@ spec:
- name: Budget
type: integer
jsonPath: .spec.tokenBudget
- name: Ready
type: string
jsonPath: .status.conditions[?(@.type=="Ready")].status
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
Expand Down
Loading
Loading