Skip to content

Prepared contract context

pycharter.agent.preparation.prepare_contract_context composes an exact contract-store envelope with the existing governed semantic context builder. It returns a JSON-compatible view with the namespace, name, active version, model-ready JSON Schema, approved field meanings, scheme version, policy status, warnings, and two fingerprints. Direct Python, CLI, and local MCP preview calls return recorded: false; the separate hosted HTTP preparation path persists an event and returns a scoped preparation_id.

The caller supplies an exact namespace (use None for the global namespace). Without a version, the service chooses the highest active semantic version in that namespace. An explicit deprecated, draft, or retired version is rejected. Existing name-only validator and MCP tools retain their shipped resolution behavior.

The contract fingerprint is SHA-256 of canonical UTF-8 JSON containing the exact namespace, name, version, and normalized full store document. The effective context fingerprint also covers the visible schema, approved field projection, exact scheme version, requester principal/role/workspace, policy decisions, runtime-only assertions, profile omissions, and omitted fields. Both use sorted keys, compact separators, Unicode output, and reject non-finite numbers. Observed evidence is omitted because it changes independently of reviewed meaning.

An authenticated host may pass PreparationScope with its verified principal, role, contract namespace, and concept-scheme workspace UUID, plus a semantic read session. The requested namespace must equal that granted namespace; the workspace scheme must exist in the same namespace. Only approved bindings in that workspace become advisory field meanings. Unapproved, deprecated, protected, or unbound meanings are marked unavailable. The model schema retains structural fields while PII and restricted field descriptions, defaults, examples, enums, constants, regex patterns, and extension text are removed recursively, including on classified nested paths. A declared ai_usage_policy.pii_redaction_required appears as a warning and is included in the effective fingerprint. Schema constraints are identified separately from reviewed advisory meaning; ODCS descriptive constraints do not become executable rules. The field summary separately lists compiled pycharter validations under enforced_rules. Warning-severity validations appear under warning_rules: they execute but do not fail a record. The model-ready schema omits pycharter extensions, while these lists preserve the rule status. Rule parameters on classified fields are redacted. runtime_assertions reports executable Draft 2020-12 assertions omitted from the provider-ready schema, such as dependentRequired and if/then. Its values are withheld on classified paths, and root-level values are withheld when the contract has any classified field. This manifest is part of the effective context fingerprint.

Imported ODCS guidance never becomes field meaning or model instructions. Model/tool schemas withhold imported property descriptions after review too. A full authenticated preparation may add an import_guidance bundle, labelled imported_reviewed_advisory, when independent durable approval evidence matches its exact namespace, name, version, and content fingerprint. A distinct host-authenticated reviewer must approve that whole version bundle. Metadata claims alone, local CLI --as labels, and legacy contract_reviewed status grant no guidance access. Direct store/API publication is still permitted without approval; its unreviewed guidance stays withheld. Offline preparation, compact/budgeted views, and bundles with any private field omit the guidance. Visible guidance is included in the effective context fingerprint. See ODCS review policy.

Local callers can pass local_only=True and omit the scope and session to receive a contract-only view with an explicit semantic_context_unavailable warning. Omitting both authenticated scope and local_only=True is refused. A host must build PreparationScope from verified identity, never from a model or tool argument.

profile="full" returns the governed view without size reduction when no budget is supplied. profile="compact" removes optional advisory text first. An optional budget_tokens estimates serialized JSON at four characters per token and then removes optional text and independent optional root fields before required-field descriptions. Composed schemas and runtime-only assertions keep their root fields intact. Required fields, executable constraints, and the runtime_assertions manifest remain intact. If the budget is below that floor, preparation raises ValueError instead of emitting an invalid schema. Full and compact results share contract identity and policy rules; the effective fingerprint covers the actual projection returned.

The MCP catalog exposes prepare_contract_context with contract_ref, namespace, profile, and budget_tokens arguments. Without a trusted server-bound preparation scope it returns a local contract-only view. A stdio operator can bind a principal, namespace, and workspace at server startup:

pycharter agent serve --role viewer --principal-id analyst-1 \
  --namespace team --workspace <scheme-uuid>

The model cannot supply principal, role, or workspace through the tool call. The authenticated remote Streamable HTTP MCP server also exposes this read tool. It binds the namespace, principal, role, and workspace from the bearer token on each request, and refuses a namespace override or a workspace outside the contract namespace. By default this projection has recorded: false and creates no preparation ID. When the host injects hosted verification, remote preparation is recorded and exact validate_record becomes available; see Authenticated remote MCP. pycharter agent prepare prints the same projection as a JSON dry run. Use --local-only for a contract-only preview, or supply a requester explicitly:

pycharter agent prepare orders@1.0.0 --namespace team --local-only
pycharter agent prepare orders@1.0.0 --namespace team \
  --principal-id analyst-1 --role viewer --workspace <scheme-uuid>

Both commands accept --profile, --budget-tokens, and --database-url. The CLI requester flags are operator input for previewing a view. Hosted authorization and durable preparation use the HTTP path described in Agent verification metadata store.

The current mapping projection identifies contracts by name and version, not namespace. If more than one namespace holds that name/version, this service omits the semantic projection and returns ambiguous_namespace_bindings. The global contract is refused when its exact store lookup is ambiguous. Namespace-qualified mappings and hosted namespace entitlement are tracked in the single backlog before semantic preparation can be enabled for such twins.

Offline verification

pycharter.agent.verification.verify_prepared_record accepts an exact name@version, namespace, candidate JSON object, and the context fingerprint returned by prepare_contract_context. Call it with local_only=True for an offline contract-only workflow. It recomputes the preparation and compares the fingerprint and exact store document before validating. A name without a version returns unresolved_version; a stale fingerprint returns stale_context. The result has accepted_unchanged, accepted_after_coercion, or rejected as its verdict, plus a reporting direction of agent_output or tool_output. Direction does not affect rules.

Diagnostics contain only declared field paths, rule codes, and generic expectations. They never include candidate values or validator exception text. Undeclared field names become <undeclared>. The feedback is deterministic and capped at 50 diagnostics. The exact path uses strict model types to avoid implicit type changes; only declared coercion_rules can normalize a record. If coercion changes the submitted record, the verifier requires allow_normalized_record=True and returns normalized_record, the exact object the caller must use. Without that authorized handoff it rejects with normalization_handoff_required. Other implicit normalization is rejected.

The MCP validate_record tool opts into this path when given context_fingerprint alongside an exact contract_ref. It does not offer a normalized-record handoff, so a coerced record is rejected there. Calls without context_fingerprint retain the existing is_valid, errors, and diagnostics response and version resolution. Offline results have recorded: false; they are not durable receipts. Hosted verification uses pycharter.agent.hosted_verification and its HTTP route to load a durable preparation, recheck scope and context, and persist a metadata-only receipt. A caller-supplied fingerprint alone is never hosted proof.

Bounded offline repair with one pinned version

Build the repair validator from the exact version returned by preparation. Pass its namespace explicitly when contracts in different namespaces share a name and version. The existing repair_loop and arepair_loop keep one validator through their bounded attempts; they do not resolve latest again. Before using a repaired record, verify its normalized data against the original context fingerprint:

from pycharter import Validator
from pycharter.agent import repair_loop
from pycharter.agent.preparation import prepare_contract_context
from pycharter.agent.verification import verify_prepared_record

prepared = prepare_contract_context(
    contracts, name="orders", namespace="team", local_only=True
)
ref = prepared["contract"]
exact_ref = f'{ref["name"]}@{ref["version"]}'
validator = Validator.from_store(
    exact_ref, store=contracts, namespace=ref["namespace"]
)
outcome = repair_loop(
    emit, validator, max_attempts=3, on_exhaust="quarantine", contract=exact_ref
)
if outcome.is_valid and outcome.data is not None:
    checked = verify_prepared_record(
        contracts,
        contract_ref=exact_ref,
        namespace=ref["namespace"],
        candidate=outcome.data,
        context_fingerprint=prepared["context_fingerprint"],
        local_only=True,
    )
    if checked["verdict"] != "accepted_unchanged":
        raise RuntimeError("prepare again before using this record")
else:
    # Store outcome.dead_letter only in a trusted quarantine sink.
    pass

emit is the caller's bounded model or tool callback; PyCharter does not choose a model. max_attempts is the total number of candidate evaluations. On exhaustion, on_exhaust="raise" raises RepairExhaustedError, "return" returns a failed result, and "quarantine" returns a DeadLetterRecord containing the failed candidate for a trusted DLQ. A new contract version does not change the validator already built for the loop. A changed contract or context makes the final exact verification return stale_context, so the caller must prepare again. Hosted callers use the authenticated hosted verify route and its preparation ID for that final check.

Bounded hosted repair with durable receipts

pycharter.agent.hosted_repair.hosted_repair_loop (and the async ahosted_repair_loop) runs the same bounded loop through hosted verification instead of a local validator. Every attempt is verified under one hosted preparation and one correlation ID, with attempt_number and attempt_id counting from 1. Each verified attempt therefore writes its own receipt, and receipt conformance reports repaired acceptance separately from first-pass acceptance. The caller supplies the transport as verify(attempt). This can be an in-process verify_hosted_record call, a POST /api/v1/agent/verify request, or the remote MCP validate_record tool:

from pycharter.agent.hosted_repair import hosted_repair_loop
from pycharter.agent.hosted_verification import verify_hosted_record


def verify(attempt):
    return verify_hosted_record(
        contracts,
        ledger,
        session,
        scope,
        preparation_id=prepared["preparation_id"],
        contract_ref=exact_ref,
        context_fingerprint=prepared["context_fingerprint"],
        candidate=attempt.candidate,
        correlation_id=attempt.correlation_id,
        attempt_id=attempt.attempt_id,
        attempt_number=attempt.attempt_number,
    )


outcome = hosted_repair_loop(emit, verify, max_attempts=3, on_exhaust="quarantine")
if outcome.is_valid:
    use(outcome.record)  # normalized bytes after declared coercion
elif outcome.refusal:
    ...  # e.g. stale_context: prepare again; *_unavailable: retry later

A result without a verdict is a refusal. This covers a raised HostedVerificationError, an HTTP error body, or an MCP error. A refusal stops the loop with refusal set to its stable code and makes no further model calls, because a model cannot repair a changed contract or an unavailable ledger. A rejected verdict feeds its value-free agent_feedback to the next emit call. After declared coercion is accepted, record holds the normalized_record the host must use, never the original candidate. When all attempts are rejected, on_exhaust returns the result (the default), quarantines the last candidate as a DeadLetterRecord (stage="agent_repair", with correlation ID and contract metadata) for a trusted sink, or raises HostedRepairExhaustedError. Rerunning with the same correlation ID and identical candidates replays the original receipts.