Skip to content
Draft
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
4 changes: 2 additions & 2 deletions apps/dashboard/app/(dashboard)/extensions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type PackageSourceConnection,
} from "../configuration/package-manager";
import type { McpByHarness, McpServer } from "../configuration/mcp-manager";
import type { HarnessMetadataResponse } from "@/lib/harness-metadata";
import { effectiveHarnesses, type HarnessMetadataResponse } from "@/lib/harness-metadata";

type PackageOverride = {
enabled?: boolean;
Expand Down Expand Up @@ -60,7 +60,7 @@ export default async function Extensions() {
overrides={overrides}
connections={connections}
mcp={mcp}
harnesses={metadata.harnesses}
harnesses={effectiveHarnesses(metadata)}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export function HarnessConfigEditor({
<TableHeader className="bg-background/35 text-muted-foreground">
<TableRow>
<TableHead>Harness</TableHead>
<TableHead>Supported versions</TableHead>
<TableHead>Adapter interval / verified ceiling</TableHead>
<TableHead>Allowed versions</TableHead>
<TableHead>Managed config</TableHead>
<TableHead className="text-right">Actions</TableHead>
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/app/(dashboard)/harnesses/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { api, requireAdminIdentity } from "@/lib/api";
import { HarnessConfigEditor } from "./harness-config-editor";
import type { HarnessMetadataResponse } from "@/lib/harness-metadata";
import { effectiveHarnesses, type HarnessMetadataResponse } from "@/lib/harness-metadata";

type HarnessManagedConfigs = {
revision: string;
Expand All @@ -22,7 +22,7 @@ export default async function Harnesses() {
configurations={value.configurations}
versionRequirements={value.version_requirements}
unverifiedOverrides={value.allow_unverified_versions}
harnesses={metadata.harnesses}
harnesses={effectiveHarnesses(metadata)}
/>
);
}
15 changes: 15 additions & 0 deletions apps/dashboard/lib/harness-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ export type HarnessMetadata = {
export type HarnessMetadataResponse = {
contract_version: number;
harnesses: HarnessMetadata[];
effective_verified_ceilings?: Record<string, Record<string, string>>;
known_versions_source?: "compiled" | "public_manifest";
known_versions_refreshed_at?: string | null;
};

export function effectiveHarnesses(response: HarnessMetadataResponse): HarnessMetadata[] {
return response.harnesses.map((harness) => ({
...harness,
generations: harness.generations.map((generation) => ({
...generation,
verified_before:
response.effective_verified_ceilings?.[harness.key]?.[generation.profile] ??
generation.verified_before,
})),
}));
}
14 changes: 14 additions & 0 deletions apps/docs/next/concepts/harness-version-compatibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,20 @@ Blue records the selected profile, normalized version, and generated paths in ha

See [Governance configuration](/next/reference/governance-config) for the complete schema and [Troubleshooting](/next/cli/troubleshooting) for reconciliation diagnostics.

## Refreshing certification without a client release

When a tested upstream release uses the existing compiled profile, maintainers
update `tests/e2e/agents.lock.json` and root `known-agent-versions.json`
together. The Control API fetches and validates this data-only manifest and
sends effective exclusive ceilings through governance config; clients never
fetch it directly. Layout or behavior changes still require a new `VersionSpec`
and a Blue release.

Operators may override `control_api.known_versions.url` with an HTTPS mirror,
change `refresh_seconds`, or set `enabled: false`. Failed refreshes retain the
last valid in-memory snapshot; after restart the fallback is the stricter
compiled ceiling. `allow_unverified_versions` remains the explicit escape hatch.

## Install an absent agent

In a Unix terminal, `blue <agent>`, bare `blue`, and `blue agent` offer
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/next/development/harness-adapter-architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,8 @@ Before review, verify these invariants:
- Old specs, operation sets, fixtures, and golden plans remain present.

Run the complete repository checks documented in [Contributing](/next/development/contributing).

For certification without an adapter behavior change, update the E2E agent lock
and root `known-agent-versions.json` together. The manifest can only extend the
exact compiled profile; it cannot change selection, capabilities, or intervals.
Behavioral or layout changes require the full generation workflow and a release.
15 changes: 10 additions & 5 deletions crates/gh-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ use std::collections::{BTreeMap, BTreeSet};
use gh_common::{GhError, Harness};
use gh_config::{
acquire_revision_locks, preflight_packages, preflight_packages_with_fetcher,
resolve_compatibility, teardown_inactive_packages, write_harness_with_package_fetcher,
write_harness_with_packages, AuthenticatedPackageFetcher, HarnessWrite, PackageFetcher,
ProfileStatus, WriteOptions,
resolve_compatibility_with_effective, teardown_inactive_packages,
write_harness_with_package_fetcher, write_harness_with_packages, AuthenticatedPackageFetcher,
HarnessWrite, PackageFetcher, ProfileStatus, WriteOptions,
};
use gh_harness::HarnessInventory;
use gh_service::{GovernanceConfig, HarnessPolicy, ServiceClient, Session};
Expand All @@ -27,6 +27,7 @@ pub struct HarnessReconcile {
/// Annotate a PATH inventory with the exact compatibility decision that will
/// be used by reconciliation and launch.
pub fn evaluate_inventory(config: &GovernanceConfig, inventory: &mut HarnessInventory) {
let ceilings = gh_config::EffectiveVerifiedCeilings(config.effective_verified_ceilings.clone());
let empty = HarnessPolicy::default();
for entry in &mut inventory.entries {
if !(entry.api_allowed && entry.client_supported && entry.installed) {
Expand All @@ -36,11 +37,12 @@ pub fn evaluate_inventory(config: &GovernanceConfig, inventory: &mut HarnessInve
continue;
};
let policy = config.policy(&entry.name).unwrap_or(&empty);
match resolve_compatibility(
match resolve_compatibility_with_effective(
harness,
entry.version.as_ref(),
entry.raw_version.as_deref(),
policy,
&ceilings,
) {
Ok(context) => {
entry.compatibility_profile = Some(context.profile.id.to_owned());
Expand Down Expand Up @@ -100,6 +102,7 @@ fn apply_once_with_inventory_and_optional_fetcher(
let mut opts = opts;
opts.session_upload_enabled = config.session_upload.is_some();
let empty = HarnessPolicy::default();
let ceilings = gh_config::EffectiveVerifiedCeilings(config.effective_verified_ceilings.clone());

// Compatibility is a revision-wide preflight. Never begin mutating one
// harness if another required, installed harness cannot be planned.
Expand All @@ -121,11 +124,12 @@ fn apply_once_with_inventory_and_optional_fetcher(
continue;
};
let policy = config.policy(&entry.name).unwrap_or(&empty);
match resolve_compatibility(
match resolve_compatibility_with_effective(
harness,
entry.version.as_ref(),
entry.raw_version.as_deref(),
policy,
&ceilings,
) {
Err(error) => {
preflight_failures.push((harness, error.with_install_hint(policy)));
Expand Down Expand Up @@ -474,6 +478,7 @@ mod tests {
revision: "r1".into(),
contract_version: GovernanceConfig::CONTRACT_VERSION,
required_capabilities: Vec::new(),
effective_verified_ceilings: Default::default(),
minimum_client_version: None,
required_client_version: None,
ttl_seconds: None,
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 @@ -5323,6 +5323,7 @@ mod tests {
revision: revision.to_owned(),
contract_version: GovernanceConfig::CONTRACT_VERSION,
required_capabilities: Vec::new(),
effective_verified_ceilings: Default::default(),
minimum_client_version: None,
required_client_version: None,
ttl_seconds: None,
Expand Down
95 changes: 85 additions & 10 deletions crates/gh-config/src/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! renderer until its announced deprecation.

use crate::adapters::{self, HarnessImplementation, ImplementationLifecycle, VersionInterval};
use crate::known_versions::EffectiveVerifiedCeilings;
use gh_common::{GhError, Harness, InstallInvocation};
use gh_service::HarnessPolicy;
use semver::{Version, VersionReq};
Expand Down Expand Up @@ -88,7 +89,7 @@ pub enum CompatibilityFailure {
harness: Harness,
installed: Version,
profile: &'static str,
verified_before: &'static str,
verified_before: String,
},
}

Expand Down Expand Up @@ -155,6 +156,18 @@ fn select_profile(harness: Harness, version: &Version) -> Option<CompatibilityPr
pub fn supported_install(
harness: Harness,
harness_policy: &HarnessPolicy,
) -> Result<InstallInvocation, GhError> {
supported_install_with_effective(
harness,
harness_policy,
&EffectiveVerifiedCeilings::default(),
)
}

pub fn supported_install_with_effective(
harness: Harness,
harness_policy: &HarnessPolicy,
ceilings: &EffectiveVerifiedCeilings,
) -> Result<InstallInvocation, GhError> {
if harness_policy.allow_unverified_versions
&& harness_policy
Expand All @@ -181,10 +194,15 @@ pub fn supported_install(
.iter()
.rev()
.find(|registration| {
interval_intersects(
interval_intersects_with_effective(
&registration.interval,
policy.as_ref(),
harness_policy.allow_unverified_versions,
ceilings.effective(
harness.key(),
registration.interval.profile,
registration.interval.verified_before,
),
)
})
.ok_or_else(|| {
Expand All @@ -201,14 +219,16 @@ pub fn supported_install(
let upper = if harness_policy.allow_unverified_versions {
interval.before
} else {
let effective =
ceilings.effective(harness.key(), interval.profile, interval.verified_before);
match interval.before {
Some(before)
if Version::parse(before).expect("compiled interval")
< Version::parse(interval.verified_before).expect("compiled ceiling") =>
< Version::parse(effective).expect("validated ceiling") =>
{
Some(before)
}
_ => Some(interval.verified_before),
_ => Some(effective),
}
};
if let Some(upper) = upper {
Expand All @@ -226,13 +246,28 @@ pub(crate) fn interval_intersects(
interval: &VersionInterval,
requirement: Option<&VersionReq>,
allow_unverified: bool,
) -> bool {
interval_intersects_with_effective(
interval,
requirement,
allow_unverified,
interval.verified_before,
)
}

fn interval_intersects_with_effective(
interval: &VersionInterval,
requirement: Option<&VersionReq>,
allow_unverified: bool,
effective_verified_before: &str,
) -> bool {
let lower = Version::parse(interval.introduced).expect("compiled interval");
let mut upper = interval
.before
.map(|value| Version::parse(value).expect("compiled interval"));
if !allow_unverified {
let verified = Version::parse(interval.verified_before).expect("compiled verified ceiling");
let verified =
Version::parse(effective_verified_before).expect("validated verified ceiling");
if upper.as_ref().is_none_or(|value| verified < *value) {
upper = Some(verified);
}
Expand Down Expand Up @@ -469,14 +504,53 @@ pub fn resolve(
raw_version: Option<&str>,
policy: &HarnessPolicy,
) -> Result<HarnessContext, CompatibilityFailure> {
resolve_for_definition(adapters::definition(harness), version, raw_version, policy)
resolve_with_effective(
harness,
version,
raw_version,
policy,
&EffectiveVerifiedCeilings::default(),
)
}

pub fn resolve_with_effective(
harness: Harness,
version: Option<&Version>,
raw_version: Option<&str>,
policy: &HarnessPolicy,
ceilings: &EffectiveVerifiedCeilings,
) -> Result<HarnessContext, CompatibilityFailure> {
resolve_for_definition_with_effective(
adapters::definition(harness),
version,
raw_version,
policy,
ceilings,
)
}

#[cfg(test)]
pub fn resolve_for_definition(
definition: &'static adapters::HarnessDefinition,
version: Option<&Version>,
raw_version: Option<&str>,
policy: &HarnessPolicy,
) -> Result<HarnessContext, CompatibilityFailure> {
resolve_for_definition_with_effective(
definition,
version,
raw_version,
policy,
&EffectiveVerifiedCeilings::default(),
)
}

fn resolve_for_definition_with_effective(
definition: &'static adapters::HarnessDefinition,
version: Option<&Version>,
raw_version: Option<&str>,
policy: &HarnessPolicy,
ceilings: &EffectiveVerifiedCeilings,
) -> Result<HarnessContext, CompatibilityFailure> {
let harness = definition.harness;
let raw = raw_version.unwrap_or("unknown");
Expand Down Expand Up @@ -518,18 +592,19 @@ pub fn resolve_for_definition(
installed: version.clone(),
}
})?;
let verified_before =
Version::parse(profile.interval.verified_before).expect("compiled verified ceiling");
let effective_ceiling =
ceilings.effective(harness.key(), profile.id, profile.interval.verified_before);
let verified_before = Version::parse(effective_ceiling).expect("validated verified ceiling");
let unverified = version >= &verified_before;
if unverified && !policy.allow_unverified_versions {
return Err(CompatibilityFailure::UnverifiedGeneration {
harness,
installed: version.clone(),
profile: profile.id,
verified_before: profile.interval.verified_before,
verified_before: effective_ceiling.to_owned(),
});
}
let unverified_warning = unverified.then(|| format!("{harness} {version} is at or beyond Blue's exclusive certified ceiling {} for {}; continuing because allow_unverified_versions is enabled. Vendor breaking changes may produce invalid configuration", profile.interval.verified_before, profile.id));
let unverified_warning = unverified.then(|| format!("{harness} {version} is at or beyond Blue's exclusive certified ceiling {} for {}; continuing because allow_unverified_versions is enabled. Vendor breaking changes may produce invalid configuration", effective_ceiling, profile.id));
Ok(HarnessContext {
definition,
harness,
Expand Down
Loading
Loading