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
17 changes: 16 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,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 @@ -413,6 +419,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 @@ -454,7 +469,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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,11 @@ 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
backed by the Capsem Python gateway SDK (`capsem>=0.7.0`).

- 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
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
2 changes: 1 addition & 1 deletion build_system/builder/gate/managedrestart.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ def fragment(plan: Plan, config: GateConfig, *, after: tuple[Step, ...]) -> Step
needs=frozenset({Needs.DISK, Needs.NETWORK}),
speed=Speed.SLOW,
),
after=(*after, sdkchecks.python_environment(plan, config)),
after=(*after, sdkchecks.python_environment(plan, config, project=config.sdk_python.project)),
)
106 changes: 84 additions & 22 deletions build_system/builder/gate/sdkchecks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,34 @@
from __future__ import annotations

from .actions import Run
from .buildschema import SourcePackageConfig
from .config import GateConfig
from .execution import Kind, Needs, Speed, Step, step
from .phase import Phase
from .plan import Plan
from .pythonenv import uv_run


def python_environment(plan: Plan, config: GateConfig, *, after: tuple[Step, ...] = ()) -> Step:
"""The Python SDK's environment, shared by every lane that uses it:
def python_environment(
plan: Plan,
config: GateConfig,
*,
project: str,
after: tuple[Step, ...] = (),
prefix: str = "sdk.python",
) -> Step:
"""A Python package's environment, shared by every lane that uses it:
dependencies fetched outside the sandbox, then the project built inside it
without an isolated build, which would fetch its backend from the network."""
project = config.sdk_python.project
prewarm = plan.shared(step(
"sdk.python.prewarm",
f"{prefix}.prewarm",
Run(["uv", "sync", "--project", project, "--frozen", "--no-install-project"], outside_sandbox=True),
kind=Kind.COMPILE,
needs=frozenset({Needs.DISK, Needs.NETWORK}),
speed=Speed.FAST,
), after=after)
return plan.shared(step(
"sdk.python.sync",
f"{prefix}.sync",
Run(["uv", "sync", "--project", project, "--frozen", "--no-build-isolation"]),
kind=Kind.COMPILE, needs=frozenset({Needs.DISK}), speed=Speed.FAST,
), after=(prewarm,))
Expand All @@ -37,17 +44,27 @@ def braavos(plan: Plan, phase: Phase, config: GateConfig, *, after: tuple[Step,
contends=(config.exclusive("workspace_binaries"),),
kind=Kind.COMPILE, needs=frozenset({Needs.DISK}), speed=Speed.SLOW,
), after=after)
synced = python_environment(plan, config)
python = phase.shared(python_package(config), after=(synced,))
synced = python_environment(plan, config, project=config.sdk_python.project)
inspect_env = python_environment(
plan, config, project=config.integrations_inspect_ai.project, prefix="integrations.inspect-ai"
)
python = phase.shared(python_package(config.sdk_python), after=(synced,))
inspect_pkg = phase.shared(
python_package(config.integrations_inspect_ai, step_prefix="integrations.inspect-ai"),
after=(inspect_env,),
)
typescript = phase.shared(typescript_package(config), after=after)
warmed = phase.shared(typescript_prewarm(config), after=(typescript,))
return python, example, typescript, warmed
return python, inspect_env, inspect_pkg, example, typescript, warmed


def python_package(config: GateConfig) -> Step:
settings = config.sdk_python
def python_package(
settings: SourcePackageConfig,
*,
step_prefix: str = "sdk.python",
) -> Step:
return step(
"fast.sdk.python.build",
f"fast.{step_prefix}.build",
Run(["uv", "run", "--project", settings.project, "--frozen", "--no-sync",
"python", "-m", "build", "--no-isolation", "--outdir", settings.build_output,
settings.project]),
Expand All @@ -74,31 +91,76 @@ def typescript_prewarm(config: GateConfig) -> Step:
)


def fragment(plan: Plan, config: GateConfig, *, after: tuple[Step, ...]) -> tuple[Step, ...]:
settings = config.sdk_python
phase = plan.phase("fast.sdk.python")
def _python_package_fragment(
plan: Plan,
config: GateConfig,
*,
after: tuple[Step, ...],
settings: SourcePackageConfig,
step_prefix: str,
extra_commands: dict[str, list[str]] | None = None,
extra_tests_after: tuple[Step, ...] = (),
) -> tuple[Step, ...]:
stage = f"fast.{step_prefix}"
phase = plan.phase(stage)
prefix = ["uv", "run", "--project", settings.project, "--frozen", "--no-sync"]
synced = python_environment(plan, config, after=after)
commands = {
"generate": uv_run(config, "python", "-m", "capsem_builder.sdkgen", "--check",
"--specification", settings.specification, "--python-package", settings.source),
synced = python_environment(
plan, config, after=after, project=settings.project, prefix=step_prefix
)
commands: dict[str, list[str]] = dict(extra_commands) if extra_commands else {}
commands.update({
"lint": [*prefix, "ruff", "check", "--config", config.suites.pytest.project_manifest,
settings.source, settings.tests],
"types": [*prefix, "ty", "check", "--project", settings.project, "--error-on-warning",
"--python-platform", "all", settings.source, settings.tests],
}
})
checks = {label: phase.add(step(
label, Run(argv), kind=Kind.LINT, speed=Speed.FAST,
), after=(synced,)) for label, argv in commands.items()}
checks["build"] = phase.shared(python_package(config), after=(synced,))
plan.record_stage(checks["build"].label, "fast.sdk.python")
checks["build"] = phase.shared(python_package(settings, step_prefix=step_prefix), after=(synced,))
plan.record_stage(checks["build"].label, stage)
tested = phase.add(step(
"tests", Run(["uv", "run", "--frozen", "--no-sync", "pytest"], cwd=config.path(settings.project)),
kind=Kind.UNIT_TEST, speed=Speed.FAST,
), after=(checks["build"],))
), after=(checks["build"], *extra_tests_after))
return (*checks.values(), tested)


def fragment(
plan: Plan,
config: GateConfig,
*,
after: tuple[Step, ...],
) -> tuple[Step, ...]:
settings = config.sdk_python
return _python_package_fragment(
plan,
config,
after=after,
settings=settings,
step_prefix="sdk.python",
extra_commands={
"generate": uv_run(
config, "python", "-m", "capsem_builder.sdkgen", "--check",
"--specification", settings.specification, "--python-package", settings.source,
),
},
)


def inspect_fragment(plan: Plan, config: GateConfig, *, after: tuple[Step, ...]) -> tuple[Step, ...]:
sdk_synced = python_environment(plan, config, after=after, project=config.sdk_python.project)
sdk_built = plan.shared(python_package(config.sdk_python), after=(sdk_synced,))
return _python_package_fragment(
plan,
config,
after=after,
settings=config.integrations_inspect_ai,
step_prefix="integrations.inspect-ai",
extra_tests_after=(sdk_built,),
)


def typescript_fragment(plan: Plan, config: GateConfig, *, after: tuple[Step, ...]) -> tuple[Step, ...]:
settings = config.sdk_typescript
phase = plan.phase("fast.sdk.typescript")
Expand Down
2 changes: 2 additions & 0 deletions build_system/builder/gate/testmodules.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def fast(plan: Plan, config: GateConfig, *, after: tuple[Step, ...] = ()) -> tup
# SDK checks remain separate leaves of the consolidated source-guard
# fragment so each language reports its own failure and timing.
sdk_checked = sdkchecks.fragment(plan, config, after=(syntax,))
inspect_checked = sdkchecks.inspect_fragment(plan, config, after=(syntax,))
typescript_checked = sdkchecks.typescript_fragment(plan, config, after=(syntax, node))
rust_sdk_checked = sdkchecks.rust_fragment(plan, config, after=(syntax,))

Expand Down Expand Up @@ -218,6 +219,7 @@ def fast(plan: Plan, config: GateConfig, *, after: tuple[Step, ...] = ()) -> tup
return (
*audited,
*sdk_checked,
*inspect_checked,
*typescript_checked,
*rust_sdk_checked,
*guards.leaves,
Expand Down
2 changes: 1 addition & 1 deletion build_system/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ filterwarnings = ["error"]
# A passing test's tmp_path is removed at its teardown, so a run's scratch
# peaks at what the failing and in-flight tests hold, not the whole suite.
tmp_path_retention_policy = "failed"
pythonpath = ["..", "../tests", "../integrations/inspect-ai", "tests/gate", "tests/release", "tests/release_site"]
pythonpath = ["..", "../tests", "tests/gate", "tests/release", "tests/release_site"]
markers = [
"capability(name): qualification group selected by the candidate's declared capabilities",
"mcp: MCP black-box integration tests (require capsem-service + VM assets)",
Expand Down
31 changes: 24 additions & 7 deletions build_system/tests/gate/test_gate_functional_sdks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
from helpers.gate import PROJECT_ROOT, gate_plan

PREPARED = (
"sdk.python.sync", "functional.sdk.rust.example", "fast.sdk.python.build",
"fast.sdk.typescript.build", "fast.sdk.typescript.package-prewarm",
"sdk.python.sync",
"integrations.inspect-ai.sync",
"functional.sdk.rust.example",
"fast.sdk.python.build",
"fast.integrations.inspect-ai.build",
"fast.sdk.typescript.build",
"fast.sdk.typescript.package-prewarm",
)


Expand All @@ -36,6 +41,12 @@ def test_python_sdk_tests_cannot_start_before_the_archives_they_install_are_buil
"clean installed-package acceptance must consume this source's wheel and sdist, "
"not whichever archive a previous run happened to leave"
)
assert {"fast.sdk.python.build", "fast.integrations.inspect-ai.build"} <= _ancestors(
plan, "fast.integrations.inspect-ai.tests"
), (
"clean installed inspect-capsem acceptance must consume this source's SDK and "
"inspect-capsem wheel and sdist archives"
)


def test_typescript_package_acceptance_has_network_prewarm_outside_the_sandbox() -> None:
Expand Down Expand Up @@ -64,21 +75,24 @@ def test_functional_installed_sdks_use_current_archives_and_declared_network_pre

plan = gate_plan("test-functional")
python = "fast.sdk.python.build"
inspect_pkg = "fast.integrations.inspect-ai.build"
npm = "fast.sdk.typescript.build"
warmed = "fast.sdk.typescript.package-prewarm"
assert {python, npm, warmed} <= set(plan.labels), "Braavos needs actual installed SDK artifacts"
assert {python, inspect_pkg, npm, warmed} <= set(plan.labels), "Braavos needs actual installed SDK artifacts"
assert "sdk.python.sync" in _ancestors(plan, python)
assert "integrations.inspect-ai.sync" in _ancestors(plan, inspect_pkg)
assert npm in _ancestors(plan, warmed)
assert Needs.NETWORK in plan.step_named(warmed).needs
assert "[outside kernel sandbox]" in plan.step_named(warmed).actions[0].render()
assert "--no-isolation" in plan.step_named(python).actions[0].render()
assert "--no-isolation" in plan.step_named(inspect_pkg).actions[0].render()


def test_composed_sdk_packages_have_one_producer_and_no_dependency_cycle() -> None:
plan = gate_plan("candidate")
labels = plan.labels
for label in ("fast.sdk.python.build", "fast.sdk.typescript.build",
"fast.sdk.typescript.package-prewarm"):
for label in ("fast.sdk.python.build", "fast.integrations.inspect-ai.build",
"fast.sdk.typescript.build", "fast.sdk.typescript.package-prewarm"):
assert labels.count(label) == 1


Expand All @@ -103,8 +117,11 @@ def test_the_packages_the_suites_drive_are_among_the_installed_workspaces() -> N

def test_sdk_preparation_stays_offline_inside_the_sandbox() -> None:
plan = gate_plan("test-functional")
sync = plan.step_named("sdk.python.sync").actions[0].render()
assert "--no-build-isolation" in sync, "an isolated build fetches its backend from the network"
for label in ("sdk.python.sync", "integrations.inspect-ai.sync"):
sync = plan.step_named(label).actions[0].render()
assert "--no-build-isolation" in sync, (
"an isolated build fetches its backend from the network"
)
example = plan.step_named("functional.sdk.rust.example").actions[0].render()
assert "--frozen" in example, "cargo must not reach the registry from inside the sandbox"

Expand Down
1 change: 0 additions & 1 deletion build_system/tests/gate/test_gate_pytestsuite.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ def test_collection_is_cache_contained_strict_and_artifact_independent() -> None
def test_collection_uses_one_locked_project_for_both_roots() -> None:
collection = pytestsuite.collection(CONFIG)
rendered = " ".join(collection.render())
assert "integrations/inspect-ai/tests/" in rendered

assert rendered.count("python -m pytest") == 1
assert "tests/ build_system/tests/" in rendered
Expand Down
4 changes: 2 additions & 2 deletions build_system/tests/gate/test_gate_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ def _already_sandboxed() -> bool:
ONLINE_FAST = {
"fast.audit.dependencies",
"fast.audit.cargo",
# Exact lockfile dependency materialization. The paired install is
# explicitly offline and stays inside the kernel boundary.
# Lockfile dependency prewarm; paired install stays offline inside the sandbox.
"sdk.python.prewarm",
"fast.sdk.typescript.package-prewarm",
"fast.mcp.typescript.package-prewarm",
"integrations.inspect-ai.prewarm",
"fast.toolchain.node",
"fast.toolchain.rust",
}
Expand Down
13 changes: 13 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ coverage:
target: 90%
flags:
- python-sdk
inspect-ai:
target: 90%
flags:
- inspect-ai
typescript-sdk:
target: 90%
flags:
Expand All @@ -50,6 +54,10 @@ flags:
paths:
- sdk/python/capsem/**
carryforward: true
inspect-ai:
paths:
- integrations/inspect-ai/inspect_capsem/**
carryforward: true
mcp-server:
paths:
- mcp/typescript/src/**
Expand Down Expand Up @@ -98,6 +106,11 @@ component_management:
paths:
- sdk/python/capsem/**

- component_id: inspect-ai
name: Inspect AI Extension
paths:
- integrations/inspect-ai/inspect_capsem/**

# MITM HTTPS proxy, TLS, cert authority, domain/HTTP policy,
# AI traffic parsing (SSE, providers, pricing). All of net/
# except policy_config.
Expand Down
Loading
Loading