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
17 changes: 17 additions & 0 deletions apps/docs/next/deployment/blue-yaml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ The `governance` section is the deployment-authored baseline delivered to client
| `contract_version` | Wire contract version. Current clients support version 3. |
| `required_capabilities` | Features a client must support before accepting policy. |
| `minimum_client_version` | Operator-visible rollout floor; required when using harness version ranges or versioned package adapters. |
| `required_client_version` | Optional canonical exact SemVer recommendation for the Blue CLI. Defaults to the control API deployment version and requires `tenant_client_version_pin`. |
| `ttl_seconds` | Client cache and polling interval; defaults to 300 seconds. |
| `required` | When true, clients fail closed instead of launching from stale cached policy. |
| `allowed_harnesses` | Supported agent keys users may launch through Blue. |
Expand Down Expand Up @@ -402,3 +403,19 @@ Every source must be immutable and SHA-256 pinned. Use `platform_sources` when h
<Check>
A complete rollout has healthy service probes, no unresolved environment references, the expected governance revision in the dashboard, and matching desired/applied revisions on a test client.
</Check>

## Recommended Blue CLI release

The reference control API advertises its deployment version as the exact
recommended Blue CLI release. Set
`governance.required_client_version` to a published, compatible release to
override that default, for example during a rolling upgrade. The default is
applied when serving a document, so old revisions follow new deployments without
being rewritten. An explicit value stays fixed across rollouts. Use canonical
SemVer without a `v` prefix or range operators. `minimum_client_version` remains
informational.

The `tenant_client_version_pin` capability is required whenever a pin is served.
The `X-Blue-Required-Client-Version` header exposes the same pin on successful
config responses and capability/contract rejections (HTTP 426). Older clients
without this capability cannot consume the document.
14 changes: 14 additions & 0 deletions apps/docs/next/reference/governance-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ harnesses:
| `allowed_harnesses` | Harness names permitted by policy. |
| `harnesses` | Per-harness policy keyed by supported harness name. |
| `minimum_client_version` | Operator-visible rollout floor; it does not replace capability enforcement. |
| `required_client_version` | Optional canonical exact SemVer recommendation for the Blue CLI. Defaults to the control API deployment version and requires `tenant_client_version_pin`. |
| `packages` | Organization-selected, immutable extension packages and their harness adapters. |
| `gateway` | Global inference routing policy applied to every allowed coding agent. |
| `session_upload` | Global portable-session capture policy; compatible profiles upload integrity-checked bundles while older clients remain accepted as non-resumable raw artifacts. |
Expand Down Expand Up @@ -138,3 +139,16 @@ Standalone skills can live anywhere in an archive and do not require a native pl
The dashboard can inspect `github:owner/repository@ref` for a public GitHub repository, a public HTTPS archive, or a deployment-managed GitHub/Bitbucket connection. Public GitHub refs resolve to immutable codeload URLs; clients download public sources directly and verify their exact digest. Managed repositories are resolved to an immutable commit and mirrored into organization-scoped object storage. Governance contains an `artifact_id` and exact digest; clients use their existing harness session to request a fresh short-lived download and never receive repository credentials.

Removing a package disables it for subsequent governed launches. Unmodified owned content is deleted; locally changed content is moved to quarantine and reported as drift.

## Recommended Blue CLI release

The reference control API advertises its deployment version as the exact
recommended Blue CLI release. Set
`governance.required_client_version` to a published, compatible release to
override that default. The value remains an exact canonical SemVer, not a range.
`minimum_client_version` remains informational.

The `tenant_client_version_pin` capability is required whenever a pin is served.
The `X-Blue-Required-Client-Version` header exposes the same pin on successful
config responses and capability/contract rejections (HTTP 426). Older clients
without this capability cannot consume the document.
17 changes: 15 additions & 2 deletions apps/docs/openapi/next.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,21 @@ paths:
responses:
"200":
description: The governance config
headers:
X-Blue-Required-Client-Version:
description: Exact recommended Blue CLI release, identical to required_client_version in the document.
schema: { type: string }
content:
application/json:
schema: { $ref: "#/components/schemas/GovernanceConfig" }
"403": { description: Authenticated email has no provisioned gateway account }
"409": { description: Gateway key provisioning is required }
"426": { description: Client contract version or capabilities are insufficient }
"426":
description: Client contract version or capabilities are insufficient
headers:
X-Blue-Required-Client-Version:
description: Exact recommended Blue CLI release, provided even when the document cannot be consumed.
schema: { type: string }

/harness-metadata:
get:
Expand Down Expand Up @@ -1684,8 +1693,12 @@ components:
contract_version: { type: integer, default: 1, description: Governance wire contract version }
required_capabilities:
type: array
items: { type: string, enum: [adapter_intervals, compiled_harness_registry, transactional_reconcile, unverified_harness_versions, versioned_state] }
items: { type: string, enum: [adapter_intervals, compiled_harness_registry, tenant_client_version_pin, transactional_reconcile, unverified_harness_versions, versioned_state] }
minimum_client_version: { type: string, description: Operator-visible rollout floor; required capabilities enforce compatibility }
required_client_version:
type: string
description: Canonical exact SemVer of the recommended Blue CLI release. Defaults to the control API deployment version. Requires tenant_client_version_pin capability.
example: "0.1.0"
ttl_seconds: { type: integer, description: Client cache TTL (default 300), example: 300 }
required: { type: boolean, description: "If true, client fails closed without fresh config" }
allowed_harnesses:
Expand Down
1 change: 1 addition & 0 deletions crates/gh-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ mod tests {
contract_version: GovernanceConfig::CONTRACT_VERSION,
required_capabilities: Vec::new(),
minimum_client_version: None,
required_client_version: None,
ttl_seconds: None,
allowed_harnesses: vec!["codex".into(), "claude".into()],
harnesses: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions crates/gh-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5320,6 +5320,7 @@ mod tests {
contract_version: GovernanceConfig::CONTRACT_VERSION,
required_capabilities: Vec::new(),
minimum_client_version: None,
required_client_version: None,
ttl_seconds: None,
allowed_harnesses: vec!["codex".to_owned()],
harnesses: Default::default(),
Expand Down
18 changes: 18 additions & 0 deletions crates/gh-service/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub struct GovernanceConfig {
/// enforces whether a client may consume the document.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minimum_client_version: Option<String>,
/// Exact tenant-recommended Blue release exported by the control plane.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required_client_version: Option<String>,
/// Client-side cache TTL. `None` ⇒ operator default (see [`Self::DEFAULT_TTL_SECONDS`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_seconds: Option<u64>,
Expand Down Expand Up @@ -62,6 +65,7 @@ impl GovernanceConfig {
pub const DEFAULT_TTL_SECONDS: u64 = 300;
pub const CONTRACT_VERSION: u32 = 3;
pub const CAPABILITIES: &'static [&'static str] = &[
"tenant_client_version_pin",
"adapter_intervals",
"compiled_harness_registry",
"transactional_reconcile",
Expand All @@ -82,6 +86,20 @@ impl GovernanceConfig {
self.harnesses.get(harness)
}

pub fn validate_client_version_pin(
required: &str,
) -> Result<semver::Version, gh_common::GhError> {
let version = semver::Version::parse(required).map_err(|_| {
gh_common::GhError::config("required_client_version must be canonical exact SemVer")
})?;
if version.to_string() != required {
return Err(gh_common::GhError::config(
"required_client_version must be canonical exact SemVer",
));
}
Ok(version)
}

pub fn ensure_client_compatible(&self) -> Result<(), String> {
if self.contract_version > Self::CONTRACT_VERSION {
return Err(format!(
Expand Down
3 changes: 3 additions & 0 deletions deploy/blue.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ governance:
- gateway_inference_jwt
# Required when harness version ranges or versioned package adapters are used.
minimum_client_version: "0.1.0"
# Exact recommended release and major compatibility boundary. Same-major
# differences warn and continue; different majors block.
# required_client_version: "0.1.0"
Comment on lines +70 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment describes major-version warning/blocking behavior that isn't implemented in this control-plane PR (or visible in the client half). The field is an exact canonical SemVer pin; enforcement today is only the tenant_client_version_pin capability gate and the X-Blue-Required-Client-Version header. Please align the comment with the actual behavior or remove the unimplemented claims.

Severity 7/10 · View on dashboard

ttl_seconds: 300
required: true
allowed_harnesses: [codex, claude, kimi, opencode]
Expand Down
3 changes: 3 additions & 0 deletions deploy/consumer/blue/blue.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ governance:
# Required when gateway is enabled.
# - gateway_inference_jwt
minimum_client_version: "0.1.0"
# Exact recommended release and major compatibility boundary. Same-major
# differences warn and continue; different majors block.
# required_client_version: "0.1.0"
Comment on lines +71 to +73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment describes major-version warning/blocking behavior that isn't implemented in this control-plane PR (or visible in the client half). The field is an exact canonical SemVer pin; enforcement today is only the tenant_client_version_pin capability gate and the X-Blue-Required-Client-Version header. Please align the comment with the actual behavior or remove the unimplemented claims.

Severity 7/10 · View on dashboard

ttl_seconds: 300
required: true
allowed_harnesses: [codex, claude, kimi, opencode]
Expand Down
17 changes: 15 additions & 2 deletions deploy/contract/governance.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,21 @@ paths:
responses:
"200":
description: The governance config
headers:
X-Blue-Required-Client-Version:
description: Exact recommended Blue CLI release, identical to required_client_version in the document.
schema: { type: string }
content:
application/json:
schema: { $ref: "#/components/schemas/GovernanceConfig" }
"403": { description: Authenticated email has no provisioned gateway account }
"409": { description: Gateway key provisioning is required }
"426": { description: Client contract version or capabilities are insufficient }
"426":
description: Client contract version or capabilities are insufficient
headers:
X-Blue-Required-Client-Version:
description: Exact recommended Blue CLI release, provided even when the document cannot be consumed.
schema: { type: string }

/harness-metadata:
get:
Expand Down Expand Up @@ -1684,8 +1693,12 @@ components:
contract_version: { type: integer, default: 1, description: Governance wire contract version }
required_capabilities:
type: array
items: { type: string, enum: [adapter_intervals, compiled_harness_registry, transactional_reconcile, unverified_harness_versions, versioned_state] }
items: { type: string, enum: [adapter_intervals, compiled_harness_registry, tenant_client_version_pin, transactional_reconcile, unverified_harness_versions, versioned_state] }
minimum_client_version: { type: string, description: Operator-visible rollout floor; required capabilities enforce compatibility }
required_client_version:
type: string
description: Canonical exact SemVer of the recommended Blue CLI release. Defaults to the control API deployment version. Requires tenant_client_version_pin capability.
example: "0.1.0"
ttl_seconds: { type: integer, description: Client cache TTL (default 300), example: 300 }
required: { type: boolean, description: "If true, client fails closed without fresh config" }
allowed_harnesses:
Expand Down
142 changes: 139 additions & 3 deletions services/control-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5042,19 +5042,61 @@ async fn revoke_current_gateway_session(
Ok(StatusCode::NO_CONTENT)
}

fn default_client_version(runtime: Option<&str>) -> String {
runtime
.filter(|version| {
gh_service::GovernanceConfig::validate_client_version_pin(version).is_ok()
})
.unwrap_or(env!("CARGO_PKG_VERSION"))
.to_owned()
}

fn prepare_client_version_pin(
config: &mut gh_service::GovernanceConfig,
deployment_version: &str,
) -> Result<HeaderMap, ApiError> {
let required = config
.required_client_version
.get_or_insert_with(|| deployment_version.to_owned());
gh_service::GovernanceConfig::validate_client_version_pin(required)
.map_err(|error| ApiError::internal(error.to_string()))?;
let mut headers = HeaderMap::new();
headers.insert(
"x-blue-required-client-version",
required
.parse()
.map_err(|_| ApiError::internal("invalid client version header"))?,
);
if !config
.required_capabilities
.iter()
.any(|cap| cap == "tenant_client_version_pin")
{
config
.required_capabilities
.push("tenant_client_version_pin".into());
}
Ok(headers)
}

async fn governance_config(
State(state): State<Arc<AppState>>,
Extension(who): Extension<Principal>,
headers: HeaderMap,
) -> Result<Json<gh_service::GovernanceConfig>, ApiError> {
) -> Result<Response, ApiError> {
let row = current_config(&state.pool, who.organization_id).await?;
let revision = row.revision;
let mut config = serde_json::from_value(row.document)
.map_err(|error| ApiError::internal(format!("decoding stored config: {error}")))?;
enforce_client_capabilities(&headers, &config)?;
let runtime_version = std::env::var("BLUE_DEPLOYMENT_VERSION").ok();
let deployment_version = default_client_version(runtime_version.as_deref());
let pin_headers = prepare_client_version_pin(&mut config, &deployment_version)?;
if let Err(error) = enforce_client_capabilities(&headers, &config) {
return Ok((pin_headers, error).into_response());
}
personalize_package_config(&state.pool, &revision, who.user_id, &mut config).await?;
personalize_gateway_config(&state, &who, &mut config).await?;
Ok(Json(config))
Ok((pin_headers, Json(config)).into_response())
}
Comment on lines 5082 to 5100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This control-plane code publishes the recommended client version and requires the tenant_client_version_pin capability, but the PR title and description claim the full CLI-side feature: blocking governed commands when Blue differs from the tenant pin, including newer clients and cached policy, plus the default-no interactive install/rollback repair flow and platform-specific remedies. None of that CLI logic is present in this diff — the current branch only contains the contract/control-api surface (feat(contract): … and feat(control-api): …). The CLI enforcement implementation appears to be on a separate branch (origin/feat/client-version-enforcement, commit 32832b0).

Either merge that branch into this PR so the title/body match the code, or update the PR title and description to reflect that this is only the server-side prerequisite (feat(control-api) / feat(contract)). Leaving it as-is will confuse reviewers looking for the enforcement/repair behavior described.

Severity 7/10 · View on dashboard


async fn personalize_package_config(
Expand Down Expand Up @@ -6568,6 +6610,10 @@ fn uncertified_harness_range(config: &gh_service::GovernanceConfig) -> Option<St
}

fn validate_complete_governance(config: &gh_service::GovernanceConfig) -> Result<(), String> {
if let Some(pin) = &config.required_client_version {
gh_service::GovernanceConfig::validate_client_version_pin(pin)
.map_err(|error| error.to_string())?;
}
if config.contract_version == 0
|| config.contract_version > gh_service::GovernanceConfig::CONTRACT_VERSION
{
Expand Down Expand Up @@ -8066,6 +8112,16 @@ async fn validate_package_audiences(
}

fn stamp_version_aware_client_floor(config: &mut gh_service::GovernanceConfig) {
if config.required_client_version.is_some()
&& !config
.required_capabilities
.iter()
.any(|cap| cap == "tenant_client_version_pin")
{
config
.required_capabilities
.push("tenant_client_version_pin".into());
}
let required = config
.harnesses
.values()
Expand Down Expand Up @@ -12151,3 +12207,83 @@ mod gateway_ttl_config_tests {
);
}
}

#[cfg(test)]
mod client_version_pin_tests {
use super::*;
fn document() -> gh_service::GovernanceConfig {
serde_json::from_value(serde_json::json!({"revision":"r1"})).unwrap()
}
#[test]
fn default_client_version_uses_only_canonical_runtime_semver() {
for version in ["1.2.3", "1.2.3-rc.gabcdef0"] {
assert_eq!(default_client_version(Some(version)), version);
}

for version in [
None,
Some("development"),
Some("e2e"),
Some("v1.2.3"),
Some("1.2.03"),
] {
assert_eq!(default_client_version(version), env!("CARGO_PKG_VERSION"));
}
}
#[test]
fn old_revisions_acquire_current_deployment_pin_without_persistence() {
let stored = document();
for runtime_version in ["1.2.3", "1.2.4-rc.gabcdef0"] {
let version = default_client_version(Some(runtime_version));
let mut served = stored.clone();
let headers = prepare_client_version_pin(&mut served, &version).unwrap();
assert_eq!(headers["x-blue-required-client-version"], version);
assert_eq!(
served.required_client_version.as_deref(),
Some(version.as_str())
);
assert!(served
.required_capabilities
.iter()
.any(|cap| cap == "tenant_client_version_pin"));
}
assert!(stored.required_client_version.is_none());
assert!(stored.required_capabilities.is_empty());
}
#[test]
fn override_and_admin_validation_use_exact_recommendation() {
let mut config = document();
config.required_client_version = Some("3.2.1".into());
stamp_version_aware_client_floor(&mut config);
assert!(validate_complete_governance(&config).is_ok());
let headers = prepare_client_version_pin(&mut config, "1.2.3").unwrap();
assert_eq!(headers["x-blue-required-client-version"], "3.2.1");
config.required_client_version = Some("^3.2.1".into());
assert!(validate_complete_governance(&config).is_err());
assert!(prepare_client_version_pin(&mut config, "1.2.3").is_err());
}
#[test]
fn successful_and_rejected_responses_expose_the_same_pin() {
let mut config = document();
let pin_headers = prepare_client_version_pin(&mut config, "1.2.3").unwrap();
let rejection = enforce_client_capabilities(&HeaderMap::new(), &config).unwrap_err();
let response = (pin_headers.clone(), rejection).into_response();
assert_eq!(response.status(), StatusCode::UPGRADE_REQUIRED);
assert_eq!(
response.headers()["x-blue-required-client-version"],
"1.2.3"
);
let mut supported = HeaderMap::new();
supported.insert(
"x-blue-capabilities",
"tenant_client_version_pin".parse().unwrap(),
);
assert!(enforce_client_capabilities(&supported, &config).is_ok());
let response = (pin_headers, Json(config)).into_response();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers()["x-blue-required-client-version"],
"1.2.3"
);
}
}
Loading
Loading