Exposure Plan
The original backend contract remains unchanged. An opt-in strict OCI subprocess-deny profile adds a separate worker protocol; v1 static adapters retain their original guarantees.
Available in published stable 0.3.0. This feature is not part of the published 0.2.1 package.
An exposure plan records an explicit publication decision over static evidence; it is not an authorization grant or runtime safety proof.
Catalog → Graph → Repository Readiness
↓
Exposure Policy
↓
Exposure Plan
↓
Direct repository REST/MCP bundle
Independent contracts and pure boundary
apizr.exposure-policy/v1 and apizr.exposure-plan/v1 are new independent
contracts. Breaking selection/planning semantics require new contract versions.
Existing Catalog, Graph, Repository Readiness, IR, Inspection, interface and
execution contracts retain their existing semantics and bytes.
from apizr.exposure import ExposurePolicy, plan_exposure, plan_bytes, plan_digest
policy = ExposurePolicy.model_validate(
{
"selection": {"include": ["python:shop:calculate"]},
"interfaces": ["rest", "mcp"],
"execution": {"allowed": ["local-process", "oci-container"]},
}
)
plan = plan_exposure(catalog, graph, readiness, policy=policy)
canonical = plan_bytes(plan)
identity = plan_digest(plan)
The API takes already validated Catalog, Graph, RepositoryReadinessReport and
ExposurePolicy models. It revalidates nested models, including unchecked copies,
and verifies the readiness report against its original evidence using the
existing pure validate_report boundary. There is no scan, source parsing,
filesystem access, project import/execution, network, Git, installation, build
backend, Docker invocation, environment probe or interface generator call.
Selection and eligibility
The policy requires nonempty interfaces and execution.allowed sets. There
are no implicit interface targets or backend choices. selection defaults to
empty: an empty plan is valid and the human report warns that nothing is planned.
selection.include: exactpython:module:symbolIDs only.selection.include_all_ready: defaults tofalse; when explicitlytrue, unions all repositoryREADYIDs with the explicit includes. It never addsCONDITIONALIDs, even when conditional exposure is enabled.selection.exclude: removes IDs after all inclusions. Exclusion always wins.- Unknown IDs in either include or exclude produce blocking diagnostics, even when the same unknown ID occurs in both sets. Bare names and malformed IDs are policy errors. No fuzzy matching or module selectors exist in v1.
eligibility.allow_conditional: defaults tofalse. Explicitly selectedCONDITIONALrecords requiretrue.AMBIGUOUSandUNSUPPORTEDnever pass.
REST and MCP compatibility is represented separately. Both v1 adapters consume the shared repository-aware eligibility decision over the validated assessment. It retains local interface blockers except exactly proven local import reasons; see Repository Readiness. Exposure never lowers or reinterprets Python types. A conditional repository assessment can pass only if repository interface eligibility still holds (for example, unknown required effect evidence). The opt-in cannot bypass an unresolved local callable/type contract. All requested targets must pass; there is no partial interface selection.
Execution composition
execution.allowed is an order-independent set of direct, local-process,
and/or oci-container. execution.require reuses the Repository Readiness
control vocabulary, including time/input/output bounds, environment, working
directory, network/filesystem restrictions and OCI resource limits.
For every selected capability, the plan records the sorted intersection of:
- compatible modes in the bound Repository Readiness report;
- explicitly allowed exposure modes satisfying exposure controls through the
existing readiness
execution_compatibilityadapter.
There is no duplicated backend capability table or ranking. Readiness restrictions
cannot be widened. The default readiness policy considers local-process and OCI;
planning direct mode also requires a readiness policy that includes direct.
An empty compatible intersection blocks the whole requested plan. Requiring
subprocess_deny always blocks affected selections; PID limits do not satisfy
that requirement (#49).
An empty selection has no affected capability and remains valid.
“Contract-compatible” does not mean runtime-available. The plan has no worker image, environment values, secrets, user identities, permissions or runtime configuration. Runtime binding belongs to the bundle/runtime stage.
Complete evidence and fail-closed behavior
Published 0.4.4 requires graph_complete = true and catalog_exit_code = 0.
In Apizr 0.4.5, exposure.scope.required_evidence owns the
selected execution proof. It consumes validated, bound Catalog, Graph and
Repository Readiness without source parsing, host lookup or execution.
Roots are the exact IDs left after include/include-all-ready/exclude resolution.
A bounded, iterative closure follows calls_capability, references_capability,
imports_capability and imports_module. References remain conservative because
the callable can be invoked indirectly. Imports are distinct from calls and require
module initialization. Each capability requires its owning module and every
present package ancestor. Missing package parents use the loader's existing
synthetic namespace contract. contains does not select every function in a module.
Private dependencies need stable bindings and execution evidence, not public JSON
input/output contracts. They never become public roots. No effects are propagated.
Graph diagnostics join canonical source identities and retained declaration spans: an exact unique span owns a function-body diagnostic; other locations belong to module initialization. Known-path Catalog parse/encoding/analysis/size/read errors can be unrelated. Input, aggregate-limit, collision, unknown-path, invalid module identity and unexplained incompleteness are global. Contradictory import trees also block the entire copied source manifest. Digest/manifest inconsistencies fail canonical validation. Required diagnostics block even with conditional opt-in. Unrelated diagnostics stay in the full audit and all full-repository digests.
Shared #252 initialization facts are required for modules and package ancestors.
A nonempty module without callable Readiness evidence remains unproven. Exact
retained import locations must match Graph edges; external imports never discharge
local APIZR-READY-015. Valid module-handle/function cycles terminate. A cyclic
module-scope callable import blocks when the target definition has not yet been
reached in retained lexical initialization order. No runtime behavior is guessed.
An empty selection tolerates unrelated path diagnostics, but global corruption still blocks it. Multiple roots remain atomic. Breadth-first paths use capability neighbors before module neighbors, then canonical IDs, yielding deterministic shortest dependency paths with relative source location and typed evidence code.
ExposureRefused.diagnostics contains deterministic typed reasons for incomplete
evidence, unknown IDs, ineligible state, incompatible interface or execution.
The API returns no plan if any blocking diagnostic exists. Invalid models or
mismatched evidence raise ValueError. No partial planning policy exists in v1.
validate_plan recomputes the expected plan from bound evidence/policy and checks
all snapshots; structural schema validation alone is not evidence verification.
Snapshot and dependency boundary
Each record retains its capability ID, module, repository-relative POSIX source path, selected-scope readiness state/reasons, requested and compatible interfaces, compatible execution modes, effect snapshot and relationship evidence. Graph diagnostics, import declarations, module import context and direct relationships retain uncertainty without repair or effect propagation. Full input documents and callee readiness/effect documents are not embedded. In 0.4.5, the plan's readiness projection may discharge interface/relationship reasons proven by the required closure; the separately bound full Repository Readiness report is unchanged. Effect, execution-policy and identity reasons retain their existing precedence. The successful v1 schema and full-repository identity semantics are unchanged.
calls_capability, references_capability and imports_capability are evidence
only: selecting A that calls B does not expose B. observed_support_modules
contains the selected source modules and known direct target modules. It is not
recursive or a complete runtime dependency closure. external_modules contains
lexical external module names from the selected evidence; no standard-library,
third-party, installed or vulnerability classification is attempted.
Queries: plan.capability_ids(), plan.for_interface("mcp"), and
plan.compatible_with("oci-container") return deterministic tuples.
Identity and serialization
Validation checks graph.catalog_digest == catalog_digest(catalog),
graph.repository_digest == catalog.repository_digest, and the readiness
catalog/graph/repository digests and conclusions. The plan binds:
repository_digest;catalog_digest;graph_digest;repository_readiness_digest(which also binds readiness policy);exposure_policy_digest.
policy_bytes/policy_digest and plan_bytes/plan_digest use sorted JSON keys,
compact separators, UTF-8, finite values and exactly one trailing LF. Set-like
selectors, exclusions, interfaces, modes and controls are sorted and deduplicated.
There is no timestamp, checkout path, Git identity or self-digest. Relocation
preserves bytes. Unselected source changes in the scanned universe change bound
evidence and thus the plan digest; selected capability IDs remain stable.
Schemas are generated from typed models:
policy and
plan.
The golden under tests/fixtures/exposure/v1 selects two capabilities, retains a
call to an unexposed helper and lexical typing evidence, and leaves conditional,
unsupported and unrelated declarations unexposed. Tests compare committed bytes
and schemas on every supported Python version, and exercise selection order,
relocation, strictness, digest mutations, tampering and audit-hook non-execution.
CLI and scope
See the exposure guide. The CLI uses
one analyze_repository discovery for Catalog and Graph, assesses readiness once,
then calls the pure planner. Verification recomputes readiness conclusions from
those artifacts without rediscovery or source parsing.
#87 covers planning.
#88 adds the
direct repository bundle stage through expose build.
Governed repository execution is implemented
by #90. There is no
expose serve, RBAC, approval service or dependency-stack split. Existing issues
#28,
#49 and
#63 remain open.