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.