Skip to content

Migrate from 0.3 to 0.4

0.4.0 is being prepared; the published stable release remains 0.3.0. This guide applies to the development compiler and the upcoming 0.4.0 release. Keep the stable Quickstart for the published 0.3.0 package. See the 0.4.0 notes for the complete scope and limits.

Prepare a separate installation

Keep your existing environment and generated bundles while validating the change. Use the retained artifacts from a reviewed successful CI run; there is no need to build the plugins or manually resolve their dependencies. The following example is for Linux x86-64 / CPython 3.11 with gh, Python 3.11 and uv already installed. It downloads a candidate, not a published release. Set REVIEWED_RUN_ID to the reviewed run and retain its commit from gh run view.

gh run view "$REVIEWED_RUN_ID" --repo Alien6-Studio/outerspace-apizr
gh run download "$REVIEWED_RUN_ID" --repo Alien6-Studio/outerspace-apizr --name release-candidate --dir release/candidate
gh run download "$REVIEWED_RUN_ID" --repo Alien6-Studio/outerspace-apizr --name release-target-ubuntu-latest-3.11 --dir release/target-download
mkdir release/target
tar -xzf release/target-download/target-export.tar.gz -C release/target
python3.11 -m venv core
core/bin/python -m pip install --no-index --find-links release/target/base release/candidate/dist/outerspace_apizr-0.4.0-py3-none-any.whl

candidate.json identifies the source commit and the eight distribution filenames, sizes and SHA-256 values. target.json identifies the dependency target and every exported file; qualification.json records actual checks and measurements. Verify these records against the selected run and its checksums before installation. For published release bytes, follow the provenance verification procedure.

Other exported targets are Linux CPython 3.12/3.13/3.14 and macOS CPython 3.11/3.14; select the corresponding release-target-<runner>-<python> artifact and inspect its actual system/architecture before use. They are target-specific, not universal wheelhouses. Windows and other Python/platform combinations are not qualified by this matrix.

For an isolated CLI tool instead, the same base wheels work with:

uv tool install --python python3.11 --offline --no-index --find-links release/target/base release/candidate/dist/outerspace_apizr-0.4.0-py3-none-any.whl
pipx install --python python3.11 --pip-args="--no-index --find-links=release/target/base" release/candidate/dist/outerspace_apizr-0.4.0-py3-none-any.whl

Choose one installation method; do not run all three into your normal tool paths. Qualification uses separate disposable tool homes. pipx itself is an explicit prerequisite for its command. Plugins never require pipx inject or editing a uv tool environment. Historical extras use the extras wheelhouse with the same core wheel, for example ...whl[legacy]; they are separate from plugin dependencies.

To develop Apizr rather than consume an exported candidate, the development installation remains available. That builds new evaluation artifacts and does not substitute for the retained candidate's qualification.

For the local example below, activate the separate core environment in your working directory:

. core/bin/activate
apizr --version

The coordinated candidate wheel reports 0.4.0; the candidate commit and archive hashes identify the candidate being qualified, not an already published package. Do not substitute a nonexistent outerspace-apizr==0.4.0 index installation. Official versions and artifact hashes will be supplied by the verified release record.

Authorize a local repository

The following example is self-contained. Start in your working directory with the new environment activated. Use an absent migration-demo directory; the example keeps generated files outside the source directory.

First create two ordinary Python functions and the policies. Readiness assesses the code evidence, exposure selects public functions, and operator authority permits reading this exact source root. They are separate inputs.

mkdir migration-demo
cd migration-demo
mkdir repository
cat > repository/api.py <<'PY'
def quote(unit_price: float, quantity: int = 1) -> float:
    return unit_price * quantity
PY
cat > repository/inventory.py <<'PY'
def available(stock: int, requested: int = 1) -> bool:
    return stock >= requested
PY
cat > readiness.json <<'JSON'
{"execution":{"modes":["direct"]}}
JSON
cat > exposure.json <<'JSON'
{"selection":{"include":["python:api:quote","python:inventory:available"]},"interfaces":["rest","mcp"],"execution":{"allowed":["direct"]}}
JSON
python - <<'PY'
import json
from pathlib import Path
root = str(Path("repository").resolve())
Path("operator.json").write_text(json.dumps({
    "schema": "apizr.operator-policy/v1",
    "grants": [{
        "adapter": "repository", "operation": "analyze",
        "target": {"kind": "local", "root": root},
        "permissions": ["source.analyze"]
    }]
}))
PY

operator.json authorizes only that canonical absolute repository root. It does not authorize a neighboring directory, follow symlinks or make functions public. For your own code, explicitly choose the root and selected capability IDs; do not accept an operator policy supplied by the project or a remote client.

An old repository command without the grant now fails before source reads:

apizr expose plan repository --readiness-policy readiness.json --policy exposure.json --plan

Expect exit 2, a structured operator_policy_required refusal on stderr and no plan on stdout. Add the explicit policy to plan and generate:

apizr expose plan repository --operator-policy operator.json --readiness-policy readiness.json --policy exposure.json --plan > plan.json
apizr expose build rest repository --operator-policy operator.json --readiness-policy readiness.json --policy exposure.json --output-dir build/rest
apizr expose build mcp repository --operator-policy operator.json --readiness-policy readiness.json --policy exposure.json --output-dir build/mcp

Expect a plan selecting python:api:quote and python:inventory:available, a REST bundle with openapi.json, and an MCP bundle with mcp-tools.json and server.py. Output directories must be absent or empty. Generation already performs the necessary analysis and planning; separate scan, graph and readiness commands are optional diagnostics, not prerequisites. They accept the same --operator-policy operator.json option.

These commands generate files without running the project. To serve and call them, install each bundle's requirements.txt in its own environment and follow the REST/MCP bundle instructions. The expected function results are quote(12.5, 2) = 25.0 and available(10, 3) = true. Execution requires trusted source and dependencies; an analysis grant is not an execution safety guarantee.

Pass the same authority from Python

There is no CLI subprocess or implicit policy-file lookup. Load the policy once and pass it to the shared compiler. From migration-demo, the following produces the same plan and bundle bytes as the preceding CLI commands:

python - <<'PY'
from pathlib import Path
from apizr.compiler import prepare_exposure, render_bundle
from apizr.exposure import ExposurePolicy, plan_bytes
from apizr.operator_policy import load_operator_policy
from apizr.repository_readiness import RepositoryReadinessPolicy

prepared = prepare_exposure(
    Path("repository"),
    operator_policy=load_operator_policy(Path("operator.json")),
    readiness_policy=RepositoryReadinessPolicy.model_validate_json(
        Path("readiness.json").read_bytes()
    ),
    policy=ExposurePolicy.model_validate_json(Path("exposure.json").read_bytes()),
)
assert plan_bytes(prepared.plan) == Path("plan.json").read_bytes()
for interface in ("rest", "mcp"):
    bundle = render_bundle(prepared, interface=interface)
    assert all(
        (Path("build") / interface / name).read_bytes() == data
        for name, data in bundle.items()
    )
print("CLI/Python parity: plan, REST bundle and MCP bundle")
PY

prepare_exposure analyzes once; render_bundle uses its retained sources. The compiler reference covers writing, refusals and the lower-level filesystem APIs. Already supplied in-memory data is not a universal access-control boundary for arbitrary Python code.

Keep permissions separate

Managed operation Required operator authorization What remains separate
Local repository analysis source.analyze for the exact root Readiness and explicit exposure selection
Git acquisition and analysis git.fetch for the exact transport/target, plus source.analyze for repository/ref/subdir Resolved commit, trust inputs and acquisition limits
apizr mcp serve source.analyze for the configured local root Explicit active MCP plugin and captured session scope
apizr-oci build image.build and registry.read for the exact build target Valid inputs, immutable base and Docker controls
apizr-oci push registry.read and registry.publish for the selected image/destination Remote identity verification and tag-conflict checks
apizr-attest attest registry.read, receipt.sign, timestamp.request for the exact signing target Key identity, expected signer and independent trust
apizr-attest publish registry.read and registry.publish for the exact proof/destination Verified proof bytes and remote confirmation

Use the complete operator examples, not a local grant reused for a Git origin. Analyze the actual GitSnapshot yielded by acquire_snapshot; the Git API example retains its acquired identity rather than authorizing a random temporary path.

Keep authority outside apizr.toml, user preferences and plugin locks. A project file configures analysis and policy paths; it cannot grant permissions. With --project, the operator root must match the configured repository root, not necessarily the directory containing the TOML file.

For the analysis MCP server, provide both --project and --operator-policy. Its three tools cannot change the root or choose authority. Restart the session after policy changes; this version does not promise dynamic revocation. It remains analysis/planning only.

Regenerate MCP servers and adapt result consumers

The stable 0.3.0 generator can emit invalid MCP structured content for non-object returns. Updating Apizr does not modify an existing bundle. Regenerate into a new directory, install its declared requirements, then reconnect the client to that server. Keep the old bundle until the replacement is verified.

Python return 0.4 MCP structuredContent
25.0 {"result": 25.0}
True {"result": true}
[1, 2] {"result": [1, 2]}
None {"result": null}
{"total": 25.0} {"total": 25.0}

For a tool known to return a scalar/list/null, read the result field. Dictionary results are preserved, including dictionaries already containing a result key; do not blindly unwrap every response. REST response bodies and direct Python returns are unchanged. See the result contract.

Keep plugins and existing workflows explicit

From the directory containing release/ and core/, export a profile and install its locked wheels offline:

core/bin/apizr plugins catalog resolve --profile mcp --catalog release/target/catalog/catalogue.json --wheelhouse release/target/wheelhouse --output-dir mcp-plan --json
core/bin/apizr plugins lock check --project mcp-plan/apizr.toml --lock mcp-plan/apizr.plugins.lock.json --wheelhouse release/target/wheelhouse --json
core/bin/apizr plugins sync --project mcp-plan/apizr.toml --lock mcp-plan/apizr.plugins.lock.json --wheelhouse release/target/wheelhouse --plugins-dir release/plugins --json
core/bin/apizr plugins enable apizr-mcp --version 0.4.0 --plugins-dir release/plugins

The oci and delivery profiles follow the same resolver with their own requirements/closures. Invoking the analysis server still requires your explicit local project and operator policy. OCI operations require prepared Docker/Buildx; Attest operations need the documented Attest and, for artifact transport, ORAS binaries. Git acquisition needs Git and, for SSH, the explicit agent/trust inputs. No tool is secretly downloaded during invocation.

Existing development 0.0.0 plugin installations, locks and operator grants are not rewritten. New artifact hashes, versions and environment identities require an explicitly prepared lock, activation and matching authority. Keep the old installation until you have validated the new one.

Plugin dependencies stay outside the core. Retain your existing plugin store and activations while checking declared artifacts and locks. Installing another version does not activate it. Preview synchronization/updates with their documented --dry-run options, then choose an explicit activation. The catalog is not an automatic updater and supplies no operator grants.

Single-file/notebook commands and the historical YAML pipeline remain available. The core, generated application and plugins have different dependency environments; the mcp extra and the apizr-mcp analysis plugin are different installations. There is no need to rewrite a working legacy pipeline into apizr.toml.