Authorize Git acquisition, image builds, signing and publication
Explicit authorization required
Remote --git commands and acquire_snapshot also require an explicit Git grant.
Managed outerspace-apizr-oci build, outerspace-apizr-oci push, outerspace-apizr-attest publish and outerspace-apizr-attest attest calls require
--operator-policy. An installed, active plugin alone does not permit
building, publication or signing.
Installation puts verified wheel bytes in an isolated environment. Activation chooses one installed version. Authorization permits a particular installed build to perform one operation on explicitly selected inputs or a remote repository. None implies the next.
For prerequisites and package availability, see Install Apizr.
Authorize repository analysis
Repository analysis requires source.analyze for filesystem-backed scan, graph,
readiness, expose plan and the analysis phase of expose build rest|mcp.
Their Python APIs use the same pure decide_analysis(policy, target) decision.
This is independent of git.fetch, image construction, signing and publication.
It does not select public functions or permit business execution. These commands require an explicit
--operator-policy or the typed Python operator_policy argument.
A complete local policy:
{
"schema": "apizr.operator-policy/v1",
"grants": [
{
"adapter": "repository",
"operation": "analyze",
"target": {
"kind": "local",
"root": "/work/project"
},
"permissions": [
"source.analyze"
]
}
]
}
Replace /work/project with the canonical absolute root actually used.
The exact root is authorized, including descendants subject to scan limits,
source roots and exclusions. It does not grant /work/project-other, another
root, or an independently requested child root. All path components are opened
without following symbolic links. Use the physical path (for example
/private/tmp/project on macOS), not a symlink alias. The directory descriptor
anchors traversal; changing a pathname cannot redirect it. MCP also pins the
root's device/inode and refuses replacement. Local file edits affect later
analyses: use the repository digest guard to detect changes between MCP calls.
apizr scan /work/project --operator-policy /private/operator.json --catalog
apizr graph /work/project --operator-policy /private/operator.json --graph
apizr readiness /work/project --operator-policy /private/operator.json --report
Explicitly selected, bounded project/operator/readiness/exposure configuration may be read to determine the target and parameters. Source enumeration and reading start only after admission. No authority is discovered from a project, profile, catalogue or environment variable. Refusals exit 2 with a fixed JSON decision on stderr, no partial report on stdout.
For Git, add a separate analysis grant with target
{"kind":"git","repository":"https://git.example/team/project.git",
"reference":"refs/heads/main","subdir":"src"}. Matching uses the exact requested
repository/ref/subdirectory, never a random temporary path. Both grants are
checked before combined CLI acquisition. The Python acquisition API separately
requires git.fetch; pass its live GitSnapshot object, plus analysis policy,
to the compiler. That object is bound to the actual acquisition, resolved commit
and opened export. Invented, copied or expired snapshots are refused. A branch
grant admits its name; the resolved commit identifies what was analyzed.
subdir does not bound all objects transferred during acquisition.
MCP captures the policy and project scope once at launch, preserving them across exec and workers. Policy edits require a restart; there is no dynamic revocation. Clients cannot choose roots or new authority. See the complete MCP example.
Authorize a Git source
Git is part of the minimal core, not an installed plugin. Use a separate
adapter: git, operation: fetch grant with git.fetch permission. Complete
fictitious HTTPS and
fictitious SSH policies show every field;
replace the example address, revision, subdirectory and absolute trust references
with values you have reviewed. Never obtain this policy from the source project.
The target binds transport, repository, reference, subdir, ca_file,
ssh_agent_socket and ssh_known_hosts. Repository strings, revision names and
subdirectories match literally: no URL decoding, hostname case folding,
default-port substitution, .git stripping, wildcard or prefix matching. An SCP
relative path is not an SSH absolute path; users and ports remain distinct.
Existing Git validators still reject unsafe URLs and ambiguous branch/tag names.
Trust references in a policy are absolute paths. API/CLI relative input paths
are made absolute against the caller's working directory before the decision,
without resolving symlinks, reading contents or collapsing .. components.
The captured values are then used for acquisition. Existing SSH/CA technical
checks still run after authorization. Matching a path does not pin its bytes,
the agent's identities or a server certificate.
A branch grant authorizes that requested name, not a commit known in advance.
Git resolves it once, then analyzes and generates from that exact commit.
subdir selects the later analysis root: acquisition processes the repository
before selecting it. It is not a confidentiality boundary limiting which
objects are transferred or temporarily exported.
acquire_snapshot(..., operator_policy=load_operator_policy(Path("operator.json")))
applies the same gate as all four remote CLI commands. decide_git(policy, target)
uses a typed GitTarget and performs no I/O. Missing/invalid/insufficient policy
refuses before Git, SSH/helpers, network/agent access, trust-file reads or workspace
creation. The CLI emits the existing structured decision on stderr and exits 2;
the API raises AuthorizationDenied. Malformed source parameters retain their
fixed GitSourceError diagnostics. No plugin installation identity is fabricated.
Only the explicitly supplied policy grants authority. Neither apizr.toml,
remote files, a catalog/profile nor environment variables can select or extend it.
Local analysis commands require their own source.analyze grant; a fetch
grant does not authorize reading a local root. Acquisition limits remain separate
validated controls. The grant replaces neither server access rights nor TLS/SSH
trust and provides no build, signing or publication permission. Existing plugin
grants remain valid and do not authorize Git. See the complete
HTTPS CLI/Python journey and SSH journey.
Analysis authorization is a separate source.analyze rule; see the complete examples above.
Write the operator's policy
Download the complete operator.json example. It
contains separate image build, image publication, proof publication and signing grants with
their dependency identities. Its repeated
SHA-256 values are illustrative: they deliberately do not authorize your installed
wheels. Replace every identity with your reviewed installation's metadata.
lock_sha256 identifies the installation requirements lock; dependencies must
contain the entire installed closure with exact versions and SHA-256 values.
For a dependency-free installation, explicitly use null and [] respectively.
The official publishers currently have dependencies.
To author publication grants using actual installed identities, first install and activate the plugins using the local extension guide. Review their origin and wheel hashes. Run these commands in a directory you control; choose your actual registry and repository in the Python block:
apizr plugins list --active --json > installed.json
python3 - <<'PY'
import json
from pathlib import Path
records = json.loads(Path("installed.json").read_text())["installations"]
repository = "registry.example:5443/team/service"
grants = []
for name, operation in (("outerspace-apizr-oci", "push"), ("outerspace-apizr-attest", "publish")):
record = next(item for item in records if item["name"] == name)
identity = {key: record[key] for key in (
"name", "version", "sha256", "lock_sha256", "dependencies"
)}
grants.append({
"plugin": identity,
"operation": operation,
"repository": repository,
"permissions": ["registry.read", "registry.publish"],
})
with Path("operator.json").open("x") as output:
json.dump({"schema": "apizr.operator-policy/v1", "grants": grants}, output, indent=2)
output.write("\n")
PY
Use the same explicit --plugins-dir or --user-config for inventory and calls
if you use a nondefault store. Inspect the resulting file before using it. Reading
the inventory supplies identities; this explicit authoring step supplies permission.
Do not obtain the policy from the project being analyzed. Keep it under operator
control, outside project-controlled inputs. There is no policy discovery or
APIZR_OPERATOR_POLICY environment variable. apizr.toml, catalogs, profiles and
operation arguments cannot select or extend a policy.
The JSON document must specify schema: apizr.operator-policy/v1 and grants.
Files are regular, nonsymlink UTF-8 JSON, at most 65,536 bytes and 128 grants.
Unknown fields/versions, duplicate JSON keys, duplicate dependency names or
permissions, and invalid types are refused. Publication grants retain their existing fields and meaning:
| Field | Meaning |
|---|---|
plugin |
Canonical distribution name, exact version, primary wheel sha256, lock_sha256, full dependencies array |
operation |
push for outerspace-apizr-oci or publish for outerspace-apizr-attest |
repository |
Exact registry, optional explicit port, namespace and image repository; no tag, digest or wildcard |
permissions |
Both registry.read and registry.publish for this operation |
A rebuilt wheel with the same name/version, or a changed lock/dependency set, needs a new grant. Reordering dependencies does not change identity. A grant must contain both required permissions; separate partial grants are not combined. No grant contains credentials, private key bytes or a free-form command. Signing grants add explicit references as described below.
Authorize an image build
Managed outerspace-apizr-oci build requires its own operation: build grant. The complete
operator.json example includes one REST build rule.
Replace its fictitious plugin and base hashes with reviewed values. To grant MCP
construction too, add a distinct rule with that interface and its exact inputs/tag.
Keep the existing publication and signing rules separate.
Each build grant contains plugin, operation, target and permissions.
plugin uses the full installed identity and locked closure described above.
Use apizr plugins list --active --json to obtain those identities from the same
store used for invocation, then review them and explicitly choose the target.
The target object shares the builder's existing input validators:
| Target field | Required match |
|---|---|
base_image |
Exact base reference pinned by SHA-256 digest |
interface, platform |
Exact rest/mcp and linux/amd64/linux/arm64 |
bundle, requirements, wheelhouse |
Exact absolute input references |
docker |
Exact executable, socket and optional Buildx path (null or omitted means none) |
tag |
Exact local output tag |
permissions must contain both image.build and registry.read in the same
matching grant. Partial grants are not combined. These rights permit neither
signature nor publication. A registry-looking local tag is still only a local
name; it does not grant permission to push. Existing v1 publication/signing
policies remain valid and do not authorize construction.
Base references keep the builder's existing forms, including
python@sha256:…, python:3.14-slim@sha256:… and registry names with ports. They
are not validated as push destinations, normalized to another registry spelling
or matched by prefix. Each input reference is compared exactly, without resolving
paths, opening files or treating nested/sibling paths as covered by a grant.
The policy authorizes references and parameters, not their bytes. The plugin
still prepares bounded private snapshots, validates bundles statically, checks
wheel hashes and installs dependencies without an index. inputs_sha256 is only
known after preparation; it is a build result, not a pre-authorized target field.
Timeout/log limits remain explicit runtime controls validated by the existing
request contract, outside the target grant. Project inputs cannot select or
extend the policy.
After preparing build.json and the inputs:
apizr plugins run outerspace-apizr-oci build --arguments build.json \
--operator-policy operator.json --timeout-ms 360000
The CLI and managed Python API refuse before plugin/tool execution, input reads
or Docker/network access. The same owned argument snapshot reaches the builder.
A successful build retains published:false, verified image/platform/input
identities and a nonprivileged image user. Apizr generates the Dockerfile; it
never executes a Dockerfile or build script supplied by the analyzed project.
apizr expose build still generates bundles and does not use this image-build
rule. Existing governed-worker policies are unchanged.
The base image or its metadata may require registry access even with cached
layers. --network=none limits Dockerfile build steps, not all daemon traffic.
This grant authorizes an invocation's parameters; it is not a daemon sandbox or
a guarantee about configured registry mirrors, DNS or redirects. Killing the
client does not confirm cancellation of daemon work: caches or unreported images
may remain. Existing cancellation, cleanup and result-verification limits apply.
Publish the selected image and its proof
Prepare push.json with the immutable local image ID, expected platform/input digest and explicit Docker/auth configuration. Prepare publish.json with the already signed proof, image digest, public trust and native verification tools.
apizr plugins run outerspace-apizr-oci push --arguments push.json \
--operator-policy operator.json --timeout-ms 360000
apizr plugins run outerspace-apizr-attest publish --arguments publish.json \
--operator-policy operator.json --timeout-ms 360000
The core validates the active installation and acquires its existing usage protection. It snapshots the arguments, validates the shared operation contract, and decides before starting a plugin, contacting Docker/a registry or reading registry credentials. The plugin receives the same checked argument snapshot; changing the original file or Python dictionary cannot substitute a destination. Authorization does not replace activation, identity, usage, TLS, image/proof verification or registry access checks. Existing interruption and unconfirmed remote-state behavior is unchanged.
Successful calls return the existing plugin response. Keep the image
digest_reference from push and artifact_reference from proof publication.
Publishing an already signed proof verifies it; it requires no private key and
no new signature permission.
Authorize signing separately
attest needs its own grant. Installation, activation or a publish grant cannot
authorize use of a signing key. The complete operator.json
includes this third rule, with fictitious identities. Use the exact reviewed
outerspace-apizr-attest identity and full locked dependency closure from your installation.
In addition to plugin, operation: attest and the exact repository, specify:
| Field | Operator expectation |
|---|---|
key_id |
Exact 32-character lowercase hexadecimal key identifier |
expected_signer |
Exact 64-character lowercase hexadecimal public signer identity |
key_file |
Exact absolute local file reference from attest.json; no private key contents |
tsa_url |
Explicit RFC 3161 authority URL from attest.json |
permissions |
All three: registry.read, receipt.sign, timestamp.request |
One matching rule must contain all three rights. Partial rules are not combined.
Signing grants do not accept registry.publish; publication grants do not accept
signing or timestamp permissions. Existing v1 publication-only documents remain
valid and do not authorize signing. Add a separate signing rule to authorize it.
Key paths are compared as exact absolute strings, without resolving symlinks,
reading files or treating /private/./operator.key as /private/operator.key.
The reference is not a cryptographic identity of the file's bytes. The native
plugin must still verify the key, actual signer, timestamp and final receipt.
Keep keys and operator-selected files protected from untrusted writers.
TSA comparison uses scheme, hostname, effective port and exact path. Scheme and
hostname are case-insensitive; omitted ports mean 80 for HTTP or 443 for HTTPS;
an empty path means /. Other paths, ports and authorities stay distinct, with
no wildcard or prefix matching. Path escapes are not decoded, and paths are not
resolved: %74imestamp and timestamp are different policy inputs. Credentials,
queries, fragments, backslashes, control characters, percent-encoded hostnames
and invalid ports are refused; the URL is limited to 2,048 characters.
Both existing HTTP and HTTPS transports remain supported; HTTPS retains TLS
verification. RFC 3161 trust still comes from independently pinned certificates.
This decision authorizes the initial URL passed to the native tool. It does not resolve DNS, pin the server's network address or impose a network sandbox on the native client's redirect behavior. Select an authority you trust and retain the existing cryptographic trust checks; authorization does not replace them.
Prepare the operational files using the delivery guide. With matching, separately reviewed signing and publication grants:
# Sign and timestamp a verified delivery.
apizr plugins run outerspace-apizr-attest attest --arguments attest.json \
--operator-policy operator.json --timeout-ms 360000
# Verify an existing receipt offline; no signing grant or private key is needed.
apizr plugins run outerspace-apizr-attest verify --arguments verify.json \
--timeout-ms 180000
# Publish that existing receipt using a distinct publication grant.
apizr plugins run outerspace-apizr-attest publish --arguments publish.json \
--operator-policy operator.json --timeout-ms 360000
Signing does not publish the proof. After signing, verification and publication can run with the private key unavailable. Publication still requires its own repository read/publish authorization. A delivery receipt remains evidence of verified delivery, not supervision of the build.
Match the destination exactly
For registry.example:5443/team/service:v1, the authorized repository is
registry.example:5443/team/service. The same grant covers other explicit tags
and SHA-256 digests in that repository, not another port, registry, path or
service-other. Short Docker Hub names such as alpine or team/service remain
ambiguous and are refused; use docker.io/library/alpine:tag or an explicit
namespace. The existing index.docker.io alias normalizes to docker.io.
Other host spellings and explicit ports retain separate identities. Credentials,
URL schemes, percent encodings, traversal and wildcards are not accepted.
Both operations read remote state before and after writing, so read-only access
is insufficient. Push's temporary apizr-upload-* tag stays in the same
repository. Attest attaches the proof to the requested image digest in that same
repository. Neither step implicitly grants access to a second repository.
Call from Python and handle decisions
from pathlib import Path
from apizr.extension_runtime import Limits
from apizr.local_plugins import read_arguments, run_extension
from apizr.operator_policy import AuthorizationDenied, load_operator_policy
try:
response = run_extension(
"outerspace-apizr-oci",
"push",
read_arguments(Path("push.json")),
operator_policy=load_operator_policy(Path("operator.json")),
limits=Limits(wall_time_ms=360000),
)
except AuthorizationDenied as error:
print(error.decision.model_dump_json(by_alias=True))
else:
print(response.model_dump_json())
decide(policy, installation, operation, arguments) is the typed, pure decision
API: no file, process, Docker or network access. It returns
apizr.operator-decision/v1, an allowed boolean and a fixed code. It alone
does not authenticate an installation; use run_extension for managed execution.
Loading the explicitly chosen file is a separate operation.
CLI authorization failures exit 2, leave stdout empty and write that JSON
decision to stderr. Python raises AuthorizationDenied with the same decision.
No paths, reference values, credentials or native diagnostics appear in it.
| Code | Decision |
|---|---|
authorized |
This operation matches a complete grant |
not_required |
This operation is outside this first authorization scope |
operator_policy_required |
No policy was supplied |
operator_policy_invalid / operator_policy_unavailable / operator_policy_too_large |
Policy refused before invocation |
operator_arguments_invalid |
Operation arguments do not satisfy the shared contract |
operator_identity_denied |
Installed build/closure or official entry point does not match |
operator_operation_denied |
No matching operation grant |
operator_repository_denied |
No matching exact repository |
operator_permissions_denied |
The grant lacks a required build, registry, signing or timestamp permission |
operator_git_source_denied |
Exact Git source, revision, selection or trust references do not match |
operator_analysis_denied |
Exact analysis root or Git source does not match |
operator_source_invalid / operator_source_unavailable / operator_source_changed |
Invalid context, inaccessible/link traversal, or changed pinned identity |
operator_build_denied |
Base, platform, interface, paths, Docker parameters or local tag does not match |
operator_key_denied |
Key ID, expected signer or key file reference does not match |
operator_tsa_denied |
Timestamp authority does not match |
Understand the boundary
This is admission control for managed Apizr invocations, not a sandbox against
a malicious trusted plugin. Directly invoking a plugin, Docker/ORAS/Attest or the
low-level extension_runtime.invoke_extension outside the managed installation
path does not acquire operator authorization. Protect operator files and plugin
storage from untrusted writers; plugins still execute with the user's rights.
This control covers managed repository analysis, Git acquisition, image
build/push, proof publish and signing attest. Single-file/notebook inspection
and primitives such as scan_sources, build_graph, assess_repository and
plan_exposure process caller-supplied bytes/artifacts in memory; they cannot
infer the authority under which those bytes were obtained. render_bundle uses
retained bytes without rereading sources. This is not universal protection against
arbitrary Python executed with the user's rights. Verification and plugin lifecycle
operations retain their controls. Publication through MCP is not added.
Verify local source permissions
The following example is self-contained. Start in your working directory with the
Apizr environment activated. Use an absent permissions-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 permissions-demo
cd permissions-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.
A repository command without the grant 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 permissions-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.
See proof-required delivery admission for explicit pre-build requirements and the separately authorized final promotion.
See multi-destination delivery for sequential managed orchestration, independent outcomes and explicit resume of one immutable build.