Skip to content
Closed
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: 16 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ jobs:
COVERAGE_FILE: ${{ github.workspace }}/cache/target/coverage/python-sdk/.coverage
run: uv run --frozen pytest --junitxml=../../cache/target/coverage/junit/python-sdk.xml

- name: Inspect AI extension tests with coverage
working-directory: integrations/inspect-ai
env:
COVERAGE_FILE: ${{ github.workspace }}/cache/target/coverage/inspect-ai/.coverage
run: uv run --frozen pytest --junitxml=../../cache/target/coverage/junit/inspect-ai.xml

- name: Python lint and type check
run: |
uv run --project build_system --frozen capsem-gate lint
Expand Down Expand Up @@ -414,6 +420,15 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

- name: Upload Inspect AI extension coverage
if: ${{ !cancelled() }}
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac
with:
files: cache/target/coverage/inspect-ai/coverage.xml
flags: inspect-ai
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

- name: Upload Rust unit test coverage
if: ${{ !cancelled() }}
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac
Expand Down Expand Up @@ -455,7 +470,7 @@ jobs:
if: ${{ !cancelled() }}
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3
with:
files: cache/target/coverage/nextest/ci-unit/junit.xml,cache/target/coverage/nextest/ci-integration/junit.xml,cache/target/coverage/junit/frontend.xml,cache/target/coverage/junit/python-cross-system.xml,cache/target/coverage/junit/python-build-system.xml,cache/target/coverage/junit/python-sdk.xml,cache/target/coverage/junit/typescript-sdk.xml,cache/target/coverage/junit/mcp-typescript.xml
files: cache/target/coverage/nextest/ci-unit/junit.xml,cache/target/coverage/nextest/ci-integration/junit.xml,cache/target/coverage/junit/frontend.xml,cache/target/coverage/junit/python-cross-system.xml,cache/target/coverage/junit/python-build-system.xml,cache/target/coverage/junit/python-sdk.xml,cache/target/coverage/junit/inspect-ai.xml,cache/target/coverage/junit/typescript-sdk.xml,cache/target/coverage/junit/mcp-typescript.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/fast-gate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jobs:
run: |
uv sync --project build_system --frozen
python3 build_system/scripts/ci/run-bounded-command.py --timeout-seconds 300 -- uv sync --project sdk/python --frozen --no-install-project
python3 build_system/scripts/ci/run-bounded-command.py --timeout-seconds 300 -- uv sync --project integrations/inspect-ai --frozen --no-install-project
cargo fetch --locked
for workspace in web/app web/docs web/marketing build_system/release_site sdk/typescript mcp/typescript; do
pnpm --dir "$workspace" install --frozen-lockfile
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
bodies previously only ever grew. Ephemeral sessions are deleted whole and
are unaffected.

- `integrations/inspect-ai` (`inspect-capsem-sandbox`) provides a standalone
[Inspect AI](https://inspect.aisi.org.uk/) `SandboxEnvironment` registered
under `capsem`, supporting direct VM execution (`execution_mode="vm"`,
default `code` profile), OCI workload containers, and nested Docker container
execution (`execution_mode="container"`) with `Dockerfile` build-context
staging, content-addressable image caching, and single-service Docker Compose
support (with fast-fail validation on unsupported multi-service
configurations).

- The profile catalog names its own defaults, one per runtime: the binary
compiles them from `config/profile-catalog.toml` (a runtime's default
counts only when that profile is installed), `GET /status`
Expand Down
134 changes: 13 additions & 121 deletions build_system/builder/gate/buildschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,100 +11,32 @@
from pathlib import PurePosixPath
from typing import Annotated, Literal

from pydantic import PositiveFloat, PositiveInt, StringConstraints, model_validator
from pydantic import PositiveInt, StringConstraints, model_validator

from ..cache.tools import CachedToolPolicy
from ..policy.dockerpolicy import BuildNetwork, ContainerNetwork
from .configschema import SafeToken, Strict
from .releaseschema import ReleasePairingEnvironment
from .configschema import Strict
from .qualifyschema import AuditsConfig as AuditsConfig
from .qualifyschema import DependencyAuditConfig as DependencyAuditConfig
from .qualifyschema import FunctionalConfig as FunctionalConfig
from .qualifyschema import GreyjoyConfig as GreyjoyConfig
from .qualifyschema import KingslandingConfig as KingslandingConfig
from .qualifyschema import ModulesConfig as ModulesConfig


class SdkConfig(Strict):
class SourcePackageConfig(Strict):
project: str
manifest: str
specification: str
source: str
tests: str
build_output: str


class NodePackageConfig(Strict):
project: str
class SdkConfig(SourcePackageConfig):
specification: str


class ModulesConfig(Strict):
build_chain_artifact_tests: tuple[str, ...]
release_suites: tuple[str, ...]
contract_globs: tuple[str, ...]
rust_format: tuple[str, ...]
rust_coverage: tuple[str, ...]
rust_coverage_floors: tuple[str, ...]
rust_coverage_report: str
rust_coverage_ratchet: str
rust_coverage_workspace_manifest: str
rust_coverage_crate_minimum: PositiveFloat
rust_coverage_ratchet_headroom: PositiveFloat
rust_coverage_crate_floors: dict[str, float]
rust_coverage_platform_crate_floors: dict[Literal["Darwin", "Linux"], dict[str, float]]
rust_test_profile_variable: str
rust_test_profile: str
rust_doctests: tuple[str, ...]
guest_binary_tests: tuple[str, ...]
release_input_dir: str
release_profile: str
release_package: str
verify_inputs_script: str
prove_profile_assets_script: str
glowup_script: str
macos_glowup_script: str
platform_support_script: str
macos_glowup_report: str
macos_report_variable: str
glowup_work_dir: str
release_bin_dir: str
default_bin_dir: str
#: The local rehearsal of the release lane's pulled path. Every path is
#: named here rather than derived from `rehearsal_work_dir`, because the
#: plan and the script both have to agree on all four and a shared root
#: with two independent join rules is two spellings of one fact.
rehearsal_script: str
rehearsal_channel: str
rehearsal_work_dir: str
rehearsal_inputs_dir: str
rehearsal_package: str
rehearsal_content_root: str
rehearsal_glowup_work_dir: str
rehearsal_before_inputs: str
rehearsal_after_manifest: str
release_pairing: ReleasePairingEnvironment


class KingslandingConfig(Strict):
fixture_script: str
fixture_dir: str
suite_path: str
benchmark_paths: tuple[str, ...]


class GreyjoyConfig(Strict):
suite_path: str


class FunctionalConfig(Strict):
kingslanding: KingslandingConfig
greyjoy: GreyjoyConfig
injection_script: str
integration_script: str
binary: str
assets_dir: str
config_root: str
profiles_subdir: str
node_workspaces: tuple[str, ...]
sdk_rust_example: tuple[str, ...]
binary_variable: str
assets_variable: str
config_root_variable: str
assets_dir_variable: str
class NodePackageConfig(Strict):
project: str


class HostImageConfig(Strict):
Expand Down Expand Up @@ -212,46 +144,6 @@ def _workspace_is_profile_and_arch_scoped(self) -> ImageBuildConfig:
return self


class DependencyAuditConfig(Strict):
"""One maintained scanner over the configured dependency lockfiles."""

cache_stage: SafeToken
lockfiles: tuple[str, ...]
config: str
scanner_args: tuple[str, ...]
timeout_seconds: PositiveInt
tool: CachedToolPolicy

@model_validator(mode="after")
def lockfiles_are_explicit_source_inputs(self) -> DependencyAuditConfig:
if not self.lockfiles or len(self.lockfiles) != len(set(self.lockfiles)):
raise ValueError("dependency audit lockfiles must be non-empty and unique")
for configured in (*self.lockfiles, self.config):
path = PurePosixPath(configured)
if path.is_absolute() or ".." in path.parts or path.name == "":
raise ValueError("dependency audit lockfiles must be repository-relative")
if self.scanner_args[:2] != ("scan", "source"):
raise ValueError("dependency audit scanner must use the source scan command")
return self


class AuditsConfig(Strict):
cargo: str
dependencies: str
dependency_drift: str
public_surface: str
source_syntax: str
hardcoded_selections: str
surfaces: str
docker_ignore: tuple[str, ...]
shell_severity: str
shell_ignore: tuple[str, ...]
skills_dir: str
max_skill_description_chars: PositiveInt
max_skill_body_lines: PositiveInt
dependency_policy: DependencyAuditConfig


class WebSurfacesConfig(Strict):
script: str
targets: tuple[str, ...]
Expand Down
2 changes: 2 additions & 0 deletions build_system/builder/gate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
SbomConfig,
SdkConfig,
SigningConfig,
SourcePackageConfig,
WebSurfacesConfig,
)
from .configschema import (
Expand Down Expand Up @@ -119,6 +120,7 @@ class GateConfig(Strict):
functional: FunctionalConfig
modules: ModulesConfig
sdk_python: SdkConfig
integrations_inspect_ai: SourcePackageConfig
sdk_typescript: SdkConfig
mcp_typescript: NodePackageConfig
sdk_rust: SdkConfig
Expand Down
133 changes: 133 additions & 0 deletions build_system/builder/gate/qualifyschema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""What `config/gate.toml` says about auditing and qualifying the built product.

Split from `buildschema`, which describes building, signing, and packaging the
product's artifacts and surfaces. Keeping build inputs and qualification suites
in one file left `buildschema` at the 300-line module ceiling this package
enforces on its own source.
"""

from __future__ import annotations

from pathlib import PurePosixPath
from typing import Literal

from pydantic import PositiveFloat, PositiveInt, model_validator

from ..cache.tools import CachedToolPolicy
from .configschema import SafeToken, Strict
from .releaseschema import ReleasePairingEnvironment


class ModulesConfig(Strict):
build_chain_artifact_tests: tuple[str, ...]
release_suites: tuple[str, ...]
contract_globs: tuple[str, ...]
rust_format: tuple[str, ...]
rust_coverage: tuple[str, ...]
rust_coverage_floors: tuple[str, ...]
rust_coverage_report: str
rust_coverage_ratchet: str
rust_coverage_workspace_manifest: str
rust_coverage_crate_minimum: PositiveFloat
rust_coverage_ratchet_headroom: PositiveFloat
rust_coverage_crate_floors: dict[str, float]
rust_coverage_platform_crate_floors: dict[Literal["Darwin", "Linux"], dict[str, float]]
rust_test_profile_variable: str
rust_test_profile: str
rust_doctests: tuple[str, ...]
guest_binary_tests: tuple[str, ...]
release_input_dir: str
release_profile: str
release_package: str
verify_inputs_script: str
prove_profile_assets_script: str
glowup_script: str
macos_glowup_script: str
platform_support_script: str
macos_glowup_report: str
macos_report_variable: str
glowup_work_dir: str
release_bin_dir: str
default_bin_dir: str
#: The local rehearsal of the release lane's pulled path. Every path is
#: named here rather than derived from `rehearsal_work_dir`, because the
#: plan and the script both have to agree on all four and a shared root
#: with two independent join rules is two spellings of one fact.
rehearsal_script: str
rehearsal_channel: str
rehearsal_work_dir: str
rehearsal_inputs_dir: str
rehearsal_package: str
rehearsal_content_root: str
rehearsal_glowup_work_dir: str
rehearsal_before_inputs: str
rehearsal_after_manifest: str
release_pairing: ReleasePairingEnvironment


class KingslandingConfig(Strict):
fixture_script: str
fixture_dir: str
suite_path: str
benchmark_paths: tuple[str, ...]


class GreyjoyConfig(Strict):
suite_path: str


class FunctionalConfig(Strict):
kingslanding: KingslandingConfig
greyjoy: GreyjoyConfig
injection_script: str
integration_script: str
binary: str
assets_dir: str
config_root: str
profiles_subdir: str
node_workspaces: tuple[str, ...]
sdk_rust_example: tuple[str, ...]
binary_variable: str
assets_variable: str
config_root_variable: str
assets_dir_variable: str


class DependencyAuditConfig(Strict):
"""One maintained scanner over the configured dependency lockfiles."""

cache_stage: SafeToken
lockfiles: tuple[str, ...]
config: str
scanner_args: tuple[str, ...]
timeout_seconds: PositiveInt
tool: CachedToolPolicy

@model_validator(mode="after")
def lockfiles_are_explicit_source_inputs(self) -> DependencyAuditConfig:
if not self.lockfiles or len(self.lockfiles) != len(set(self.lockfiles)):
raise ValueError("dependency audit lockfiles must be non-empty and unique")
for configured in (*self.lockfiles, self.config):
path = PurePosixPath(configured)
if path.is_absolute() or ".." in path.parts or path.name == "":
raise ValueError("dependency audit lockfiles must be repository-relative")
if self.scanner_args[:2] != ("scan", "source"):
raise ValueError("dependency audit scanner must use the source scan command")
return self


class AuditsConfig(Strict):
cargo: str
dependencies: str
dependency_drift: str
public_surface: str
source_syntax: str
hardcoded_selections: str
surfaces: str
docker_ignore: tuple[str, ...]
shell_severity: str
shell_ignore: tuple[str, ...]
skills_dir: str
max_skill_description_chars: PositiveInt
max_skill_body_lines: PositiveInt
dependency_policy: DependencyAuditConfig
Loading
Loading