Trusted context
A context contract is the versioned vocabulary of trusted facts that resolution may freeze and Cedar may read for an action. It names each fact, its Cedar type and value domain, where the fact is stored, which source kind attests it, how long evidence stays fresh and which writers a source may list. That vocabulary used to be hard-coded across resolution, the evaluator, the demo Cedar schemas and the approval page. The catalog in src/execbound/contracts.py now holds it in one place. Tenants tune freshness and writers through bundle settings; contracts change only through reviewed code.
The approved design is 2026-09-14-context-contracts-design.md. This guide follows the code where the two differ. Acceptance cases CC1 to CC12 are in the acceptance inventory.
Production contracts
PRODUCTION_CATALOG contains four contracts. The two v1 contracts reproduce earlier behavior exactly: tests/test_context_parity.py compares resolution and evaluation output byte for byte with tests/fixtures/context-contracts-v1-parity.json, which was generated from the code before the catalog existed.
| Version | Actions | Resource type / Cedar entity | Facts | Incident | Claim binding | Display fact |
|---|---|---|---|---|---|---|
endpoint-isolation.v1 | endpoint.isolate | endpoint / Endpoint | role, environment, criticality, is_production, incident_status, incident_severity | required | false | role |
endpoint-recovery.v1 | endpoint.lift_isolation | endpoint / Endpoint | Identical to endpoint-isolation.v1 | required | true | role |
identity-control.v1 | identity.disable, identity.enable, identity.revoke_sessions | identity / Identity | privilege_class, environment, criticality, is_production, incident_status, incident_severity | required | true | privilege_class |
identity-credential.v1 | identity.reset_password | identity / Identity | Identical to identity-control.v1 | required | true | privilege_class |
The two newer contracts exist because a released contract stays immutable in every field: endpoint-isolation.v1 keeps its legacy unbound incident claim, so endpoint.lift_isolation gets its own version rather than joining that contract's action set. Because their entity attributes equal the v1 contracts', all four share one bundle with one Endpoint and one Identity entity.
All four contracts are column-backed. role, privilege_class, environment and criticality come from resources; incident_status and incident_severity come from the status and severity columns of incidents. is_production is derived as environment == "production".
An operation mapping selects the contract. The owner-provisioned operation_mappings.context_contract_version column names a version, and resolution calls Catalog.for_operation(version, action, resource_type), which requires the contract to exist, serve the mapping's action and match its resource type. OperationSpec.context_contract_version in operations.py is the production default per action; the validation seed uses it when it provisions mappings. The frozen plan records the version as ResolvedRequest.context_contract_version, which is stored in executions.frozen and covered by both the plan hash and the re-attestation binding hash.
Functions that need contracts take a catalog argument that defaults to PRODUCTION_CATALOG, including Runtime, admit, decide_approval, resolve_request, resolve_snapshot, attest_snapshot, evaluate, validate_bundle, activate_bundle, load_active_bundle and simulate. A catalog also carries the action catalog as catalog.actions, defaulting to PRODUCTION_ACTIONS, so the runtime threads one object for both vocabularies. Tests inject a catalog with extra contracts, extra actions or both instead of patching globals. The gateway configuration and the replay command use the default, so a contract is usable outside tests only after it is added to PRODUCTION_CATALOG.
Catalog model
Contracts
ContextContract field | Meaning |
|---|---|
version | Identifier matching ^[a-z][a-z0-9_.-]{0,79}$, unique in the catalog |
resource_type | endpoint or identity |
cedar_entity_type | Entity name matching ^[A-Z][A-Za-z0-9]{0,62}$, used as ExecBound::<name> |
actions | Non-empty set of actions from the injected action catalog whose resource type equals resource_type |
incident | required or none |
bind_incident_claim | When true, evaluation requires the frozen incident to be the claimed incident |
display_fact | Fact shown as the resource class on the Approvals and Activity pages |
sources | One SourceDefaults for each source kind the facts use, and no others |
facts | Ordered tuple of FactSpec |
Facts, domains and Cedar types
FactSpec field | Meaning |
|---|---|
name | Matches ^[a-z][a-z0-9_]{0,62}$; tenant and account are reserved |
cedar_type | String, Long or Boolean |
domain | StringDomain(values), IntegerDomain(minimum, maximum) or BooleanDomain() |
source_kind | inventory or incident |
storage | Column(column), FactRow(), or None for a derived fact |
derivation | Equals(dependency, value) or None |
| Cedar type | Required domain | Accepted value |
|---|---|---|
String | StringDomain with at least one value, each 1 to 254 characters | A str in the set |
Long | IntegerDomain with integer bounds, minimum <= maximum, inside the signed 64-bit range | An int in the inclusive range |
Boolean | BooleanDomain | A bool |
value_matches requires the exact Python type, so "4" and True never satisfy a Long fact.
Source kinds and storage
source_kind is the context_sources.kind that must attest the fact. Storage decides where resolution reads it.
| Storage | Allowed for | Value and provenance |
|---|---|---|
Column("role"), Column("privilege_class"), Column("environment"), Column("criticality") | inventory facts | The resources row: the column, provider_id as source object, version, source_id, observed_at |
Column("status"), Column("severity") | incident facts | The linked incidents row: the column, incident_id as source object, version, source_id, observed_at |
FactRow() | inventory facts only | The resource_facts row named after the fact: value, version, source_id, observed_at, with the resource's provider_id as source object |
The permitted columns are the COLUMNS mapping in contracts.py. Incident facts are always column-backed, so a new inventory fact uses FactRow().
Incident mode and claim binding
With incident="required", the contract declares at least one incident fact. Resolution loads the incident named by claimed_context.incident_id only when it is linked to the resolved resource in the same tenant, account, provider and resource type. With incident="none", the contract declares no incident facts and resolution loads no incident; a claimed incident_id stays an untrusted claim and produces no facts.
bind_incident_claim=True adds an evaluation check that every frozen incident fact's source object equals the claimed incident_id. The catalog rejects it when incident is none, and requires it to be true whenever incident is required. endpoint-isolation.v1 is the one named exception: it keeps bind_incident_claim=False to preserve earlier behavior, because the current evaluator does not check it for that version and resolution already enforces the incident link through its query. contracts.LEGACY_UNBOUND_INCIDENT_CLAIMS lists that exempted version; a new contract that requires an incident must set bind_incident_claim=True.
Derivations
Equals(dependency, value) is the only derivation. It yields a Boolean that is true when the dependency's value equals value. A derived fact has no storage, has the same source kind as its dependency, and copies the dependency's source, version, writers, observation time and expiry with origin DERIVED. The dependency must be a resolved fact of the same contract, not another derived fact, and value must be inside its domain.
Resolution computes facts in tuple order. The catalog requires a derived fact to be listed after its dependency and raises ContractError at construction when it is listed first.
Source defaults
SourceDefaults(max_age_seconds, ceiling_seconds, permitted_writers) sets, for one source kind, the default freshness window, the largest window a bundle may set and the default writer allowlist. All four production contracts use these values:
| Source kind | Default max age | Ceiling | Default permitted writers |
|---|---|---|---|
inventory | 300 seconds | 3,600 seconds | inventory_sync |
incident | 300 seconds | 3,600 seconds | soc_analyst |
Cedar entity
Evaluation sends Cedar a principal ExecBound::Agent with attribute tenant, the action ExecBound::Action::"<action>", a context that is empty unless the action declares arguments (see Action catalog), and a resource of type ExecBound::<cedar_entity_type> with ID <tenant>/<account>/<provider>/<resource_type>/<target_id>. The resource's attributes are tenant and account as strings plus every contract fact. Caller claims never become attributes. A bundle's Cedar schema must declare that entity with exactly those attributes; see step 6 of Adding a contract.
Construction rules
Catalog(...) validates every contract and raises ContractError when:
- two contracts share a version;
- the version, entity type, resource type or incident mode is invalid;
actionsis empty, names an action missing fromcatalog.actions, or names an action for another resource type;factsis empty, or a fact name repeats, is invalid, or istenantoraccount;- a Cedar type is unsupported or its domain breaks the rules above;
- a resolved fact names a column outside
COLUMNSfor its source kind, has no storage, or usesFactRow()as an incident fact; - a derivation's dependency is missing or derived, is listed after the derived fact instead of before it, the derived fact has storage or is not
Boolean, the source kinds differ, or the value is outside the dependency's domain; display_factis not a fact of the contract;- incident facts exist with
incident="none", or none exist withincident="required"; bind_incident_claimis true without a required incident;incident="required"andbind_incident_claimis false, unless the version is listed inLEGACY_UNBOUND_INCIDENT_CLAIMS;sourcesdoes not cover exactly the source kinds the facts use;- a default does not satisfy
1 <= max_age_seconds <= ceiling_seconds <= 86400(MAX_CEILING_SECONDS); - default writers are empty, more than 16, unsorted, duplicated, or not all matching
^[a-z][a-z0-9_.-]{0,63}$.
Adding a contract
The synthetic contract in tests/context_fixtures.py is the worked example. SYNTHETIC_VERSION is endpoint-facts.test. It serves endpoint.isolate through the operation mapping endpoint.isolate.facts (SYNTHETIC_OPERATION), with three row-backed facts, one derivation and no incident. tests/test_context_execution.py runs it through the runtime to a single provider mutation.
-
Never change or remove a released version. Frozen plans, pending approvals, the Approvals and Activity pages and replay look contracts up by the version stored in each plan. A removed or altered contract makes that history deny, fail re-attestation or fail to render. Release a behavior change as a new version.
-
Check the action. Every action must already exist in the action catalog with the contract's resource type.
Catalogvalidates each contract'sactionsagainst its injectedcatalog.actions, so an unknown action raisesContractErrorat construction. Adding actions is separate work; follow Adding an action. -
Choose storage. Use
FactRow()for new inventory facts. UseColumn(...)only for the existing columns above. Incident facts can only useColumn("status")andColumn("severity"); setincident="none"when the action needs no incident. When the action needs an incident, setincident="required"andbind_incident_claim=True; the catalog rejectsincident="required"withbind_incident_claim=Falsefor any version other than the exemptedendpoint-isolation.v1. -
Define facts, sources, derivations and the display fact. List a derived fact after the dependency it reads: the catalog rejects a contract that lists a derived fact before its dependency. The synthetic contract declares String, Long and Boolean row facts, an
Equalsderivation listed after its dependency, inventory defaults andexposureas its display fact:SYNTHETIC = ContextContract(version=SYNTHETIC_VERSION,resource_type="endpoint",cedar_entity_type="Endpoint",actions=frozenset({"endpoint.isolate"}),incident="none",bind_incident_claim=False,display_fact="exposure",sources=MappingProxyType({"inventory": SourceDefaults(300, 3600, ("inventory_sync",))}),facts=(FactSpec("exposure","String",StringDomain(frozenset({"internal", "external"})),"inventory",FactRow(),),FactSpec("risk_score", "Long", IntegerDomain(0, 100), "inventory", FactRow()),FactSpec("edr_managed", "Boolean", BooleanDomain(), "inventory", FactRow()),FactSpec("is_external","Boolean",BooleanDomain(),"inventory",None,Equals("exposure", "external"),),),) -
Register it. Add the contract to
PRODUCTION_CATALOG; the tests buildCATALOG = Catalog((ENDPOINT_ISOLATION_V1, IDENTITY_CONTROL_V1, SYNTHETIC))instead.test_production_catalog_matches_operation_defaultsintests/test_contracts.pyasserts that each production contract equalsOperationSpec.context_contract_versionfor every action it serves, so a second production version for an existing action fails that test until the default is changed deliberately. -
Write the Cedar JSON schema entity. Under
ExecBound.entityTypes, declare the contract's entity type with aRecordshape whose attributes are exactlytenantandaccountasStringplus each fact with its Cedar type.requiredmay be omitted ortrue; no other attribute keys are allowed. The entity fromSYNTHETIC_SCHEMA:{"ExecBound": {"entityTypes": {"Endpoint": {"shape": {"type": "Record","attributes": {"tenant": {"type": "String"},"account": {"type": "String"},"exposure": {"type": "String"},"risk_score": {"type": "Long"},"edr_managed": {"type": "Boolean"},"is_external": {"type": "Boolean"}}}}}}}The full schema also declares the
Agententity with atenantattribute and theendpoint.isolateaction'sappliesTo, which Cedar validation and evaluation need. The contract check reads only contract entities. Contracts used by one bundle that share an entity type must declare identical attributes, soendpoint-facts.testandendpoint-isolation.v1cannot share a bundle. -
Point mappings at the version. Provision an
operation_mappingsrow whosecontext_contract_versionis the new version.seed_syntheticinsertsendpoint.isolate.factsfor providermock_crowdstrike, resource typeendpoint, provider operationcontain, mapping versionmap-v1and schemaaction.v1. The bundle'sMappingRefmust repeat the account, mapping version, schema version andcontext_contract_version. -
Provision trusted facts. Attest the facts with an active
context_sourcesrow of the fact's kind whoseallowed_writersare trusted labels inside the effective allowlist, and write oneresource_factsrow per row-backed fact per resource, normally through a manifest andcontext-import. See Writingresource_factsrows and Provisioning trusted context. -
Use it in a bundle. Policies read facts as
resource.<fact>.SYNTHETIC_AUTHORIZATIONpermits isolation when the tenant matches,resource.edr_managedis true andresource.risk_score >= 50, and forbids it whenresource.is_external.synthetic_bundle(...)assembles the bundle, which is then created, validated and activated throughpolicy/store.pywith the catalog passed in. Addcontext_settingsonly when the defaults do not fit. -
Add tests alongside the existing families:
| Test file | Add or keep |
|---|---|
tests/test_contracts.py | The production catalog still constructs and agrees with operation defaults; add rule tests if construction changes |
tests/test_context_parity.py | Must pass unchanged; never regenerate its golden fixture to accept a change |
tests/test_context_bundle_validation.py | The contract's pack schema validates, and missing, extra, mistyped or optional attributes fail |
tests/test_context_resolution.py | Facts resolve from storage with provenance and expiry; sources, writers, settings and invalid rows fail closed |
tests/test_context_evaluation.py | Facts reach Cedar, forbidden values deny, derivations and source consistency are enforced |
tests/test_context_execution.py | One provider mutation when allowed, none when denied, a changed fact row makes a pending approval stale, the display fact renders as text |
tests/test_replay_scenarios.py | Candidate settings for the contract behave as described under Freshness |
Resolution and trust
resolve_facts in resolution/facts.py builds the TrustedFact tuple for one contract and never authorizes. resolve_request and the coordinator's resolve_snapshot both call it with the rows they load.
- Facts come only from contract storage: the resource row, its
resource_factsrows and, for a required incident, the linked incident row. Rows for fact names the contract does not declare are ignored. - Each resolved fact is trusted through the source that attests its storage: the resource's source for inventory columns, the incident's source for incident columns, and the fact row's own
source_idfor row-backed facts. That source must be active, have the fact's source kind, and list a non-emptyallowed_writersset inside the effective permitted writers. A row fact moved to another source is checked against that source even when the resource's source is valid. - The value must be non-null and match the fact's Cedar type and domain.
- Caller claims never substitute for trusted facts. Resolution reads no claim except
claimed_context.incident_id, and that is only an untrusted selector for an incident already linked to the target. The incident's columns and source supply the facts. - A mapping whose contract is missing or does not serve it raises
ResolutionErrorwithINVALID_OPERATION. Any fact, source, setting or freshness failure raisesINVALID_CONTEXT.
Evaluation rechecks frozen evidence against the contract in _trusted_entities (policy/evaluate.py): exact fact names, types and domains; origin, source kind, dependencies and derivation results; sorted, unique writer tuples inside the evaluating bundle's allowlist; and source consistency. Column-backed inventory facts share one source tuple whose object and version are the resource's provider ID and version. Row-backed facts must name the resource's provider ID. Incident facts share one source tuple whose object is a canonical UUID.
Freshness
No expiry is stored. Resolution computes each resolved fact's valid_until as observed_at plus the effective max age: the bundle's setting for that contract and source kind, or the contract default. It rejects a fact when observed_at > now or valid_until <= now. A derived fact copies its dependency's times. The plan's valid_until is the earliest of the credential expiry, every fact's valid_until, the alias expiry and the run expiry, and resolution fails when now has reached it.
Evaluation applies the rule again with the evaluating bundle's settings. A frozen fact is valid only when all of these hold:
observed_at <= resolved_at <= now < valid_until;valid_until <= observed_at + effective max age;- the plan's
valid_until <= valid_until.
The plan itself must satisfy resolved_at <= now < plan valid_until <= credential expiry, and evaluation also denies with INVALID_CONTEXT when it finishes at or after the plan's valid_until. Approvals expire no later than the plan's valid_until.
A longer max age lets a newly resolved request accept an older observation (test_widened_setting_admits_fact_older_than_default_age). It cannot extend a plan that is already frozen: evaluation still requires now < valid_until for the frozen value, and re-attestation rejects a current resolution whose valid_until is earlier than the frozen one or whose facts were observed earlier. A shorter max age denies frozen facts whose valid_until exceeds the shorter window. test_longer_max_age_cannot_extend_frozen_validity and the replay candidate tests cover both directions.
Bundle context settings
BundleContent.context_settings is an optional list of ContextSetting entries, at most one per contract and source kind. An excerpt of stored bundle JSON, with the other bundle fields omitted:
{
"context_settings": [
{
"context_contract_version": "endpoint-isolation.v1",
"source_kind": "incident",
"max_age_seconds": 600,
"permitted_writers": ["soc_analyst"]
},
{
"context_contract_version": "endpoint-isolation.v1",
"source_kind": "inventory",
"max_age_seconds": 900,
"permitted_writers": ["cmdb_sync", "inventory_sync"]
}
]
}
- A setting replaces the contract default for its contract and source kind. It does not add to it: the inventory entry above keeps
inventory_syncso sources that list it stay accepted. A source kind without a setting uses the contract default. - Model rules in
policy/models.py:max_age_secondsis 1 to 86,400;permitted_writersholds 1 to 16 unique labels matching^[a-z][a-z0-9_.-]{0,63}$, stored sorted; the list holds 1 to 32 entries with unique(context_contract_version, source_kind)pairs, stored sorted by that pair. An empty list is rejected, so omit the field to use defaults. - Unset settings are omitted from the stored bytes, so bundles without settings keep their earlier canonical JSON and content hashes.
validate_bundle,activate_bundleandload_active_bundlecheck the bundle against the catalog. Each mapping's contract must exist and serve that mapping. The Cedar schema must be in Cedar's JSON schema format, and each used contract's entity must declare exactlytenant,accountand the contract's facts with their types. Each setting must name a contract used by the bundle's mappings and a source kind that contract uses, withmax_age_secondsno higher than that source's ceiling. The schema must also declare each mapped action with a context record matching the action catalog; see Cedar action contexts. Failures raisePolicyStoreErrorwithINVALID_COMPONENT.- Settings are part of the bundle content hash, and plan hashes bind that hash. Content is immutable, so changing settings means a new bundle, and activating it makes plans and approvals frozen under the previous bundle stale.
- Writer allowlists must name only trusted writer labels. A source's
allowed_writersrecords who can change its data. A bundle that admits a label the protected agent can write (the tests useagent) makes resolution accept agent-written context, which removes the protection acceptance scenario T requires.
Writing resource_facts rows
Migration 0009_context_contracts creates the table.
| Column | Constraint and meaning |
|---|---|
tenant_id, account_id, provider, resource_type, resource_id | Foreign key to the resources row the fact describes |
fact_name | Matches ^[a-z][a-z0-9_]{0,62}$; the contract's fact name |
value | jsonb scalar: a string of 1 to 254 characters, a boolean, or an integer within Cedar's signed 64-bit Long range (-9223372036854775808 to 9223372036854775807; no fraction or exponent) |
source_id | With tenant and account, a foreign key to the attesting context_sources row |
version | 1 to 80 characters; frozen as the fact's source_version |
observed_at | timestamptz; when the trusted source observed the value |
The primary key is the resource key plus fact_name, so each resource has at most one row per fact. An index covers (tenant_id, account_id, source_id).
- Encode the value for its Cedar type:
'"internal"'::jsonbforString,'70'::jsonbforLong,'true'::jsonbforBoolean. The table accepts any scalar of these shapes, but resolution fails closed on a type or domain mismatch, such as'"70"'for aLongfact. - Storage covers the whole signed 64-bit
Longrange since migration0020_resource_facts_long, so anyIntegerDomaina contract can express is storable; earlier revisions stopped at 18 digits (#20). - Tenant RLS is forced. The runtime role
execbound_apphas SELECT only and cannot insert, update, delete or truncate. - Write rows with a trusted owner or sync connection, never the runtime connection or anything the protected agent controls. Forced RLS also filters a non-bypass owner, so set
execbound.tenant_idin the transaction first, as the validation seed does for the registry. - A changed value, version or source makes a pending approval stale at re-attestation. Advancing only
observed_atkeeps the binding but does not extend a frozen plan. execbound context-importis the supported way to write rows outside fixtures: it validates each fact against the catalog and the source, keeps a row's version unchanged while its content is unchanged and audits the import; see Provisioning trusted context.seed-validationstill writes its rows directly, and tests seed rows throughseed_syntheticor manifests.- Migration 0009 refuses to downgrade while any row exists; see Data model.
BEGIN;
SELECT set_config('execbound.tenant_id', '<tenant UUID>', true);
INSERT INTO execbound.resource_facts
(tenant_id, account_id, provider, resource_type, resource_id,
fact_name, value, source_id, version, observed_at)
VALUES
('<tenant UUID>', '<account UUID>', 'mock_crowdstrike', 'endpoint', '<resource UUID>',
'risk_score', '70'::jsonb, '<inventory source UUID>', 'fact-v1', '<observation time>');
COMMIT;
Provisioning trusted context
The trusted context provisioning design is implemented in provisioning.py and the owner commands context-source-create, context-source-deactivate and context-import; the operations guide shows the commands. Acceptance cases CP1 to CP12 are in the acceptance inventory.
Two processes hold different authority. An adapter talks to the source system and writes a manifest file: network access, no database authority. The importer reads the manifest and writes rows with the owner connection inside one tenant transaction under an ADMIN actor: database authority, no network. The runtime role keeps SELECT only on context_sources, resources, incidents and resource_facts, check_runtime is unchanged, and no HTTP or MCP route reaches the importer.
A manifest (execbound.context-manifest.v1, parsed with strict models that forbid unknown fields) names the tenant, account, source, kind (inventory or incident) and writer label, then lists entries. An inventory entry names a resource by provider, resource_type and provider_id, gives a content version and a timezone-aware observed_at, facts keyed by contract fact name, optional retract names, an optional active flag and an optional provider_version. An incident entry adds incident_id and carries incident_status and incident_severity in facts. Bounds: 16 MiB, 10,000 entries, 64 facts and 32 retractions per entry, no duplicate resource or incident, no name in both facts and retract, and an entry must state something.
The importer:
- routes each fact name through the catalog to its storage for the entry's resource type and source kind, and refuses derived names (
DERIVED_FACT), names no contract declares (UNKNOWN_FACT), names that contracts store differently (AMBIGUOUS_FACT) and values with the wrong exact type or outside any declaring contract's domain (INVALID_VALUE); - requires
--tenantto equal the manifest's tenant (TENANT_MISMATCH), an active ADMIN actor (INVALID_ACTOR), an active account (INVALID_ACCOUNT), an active source of the manifest's kind (INVALID_SOURCE) and a writer that source allows (INVALID_WRITER); - skips and counts resources the registry does not hold under the account, or refuses the manifest with
--strict(UNKNOWN_RESOURCE); it never creates resources, aliases or canonical identities; - refuses an observation more than 60 seconds ahead of the clock (
FUTURE_OBSERVATION) or earlier than the retained observation of a row the entry touches (OBSERVATION_REGRESSION); - changes a
resource_factsorincidentsrow'sversionexactly when its content changes (the value, or the status and severity, and the attesting source), keeps the retained version and advances onlyobserved_atotherwise, and refuses a reused version with different content (VERSION_REUSED); a routine sync with a new snapshot label therefore leaves pending approvals valid, while a real change makes re-attestation returnSTALE_AUTHORIZATION; - treats
resources.versionas the provider's expected version, which every plan freezes asresource_versionand the provider checks atomically at dispatch: a manifest changes it only throughprovider_version, while the inventory columns, theactiveflag and the source are the row's content and change under the retained version (re-attestation still sees changed values through the frozen plan); - deletes a retracted row-backed fact, nulls a retracted column-backed fact and deactivates a resource on
active: false; each makes resolution fail closed, withINVALID_CONTEXTfor a missing fact andINVALID_TARGETfor an inactive resource; - keeps an incident bound to its first resource (
INCIDENT_RELINKED); a ticket that moves to another asset is a new incident identifier; - writes in one transaction with multi-row statements, then appends
context.importedwith the source, account, kind, writer, manifest digest, manifest size and counts, never values. A refused manifest writes and audits nothing. Re-importing the same manifest is a no-op that reports every entry asunchanged.
Sources are created with their kind and 1 to 16 writer labels, which stay fixed for the source's life, and are deactivated when retired or compromised; both changes are audited (context_source.created, context_source.deactivated), and a deactivated source makes every fact it attests fail resolution. A source's allowed_writers must still lie inside the effective permitted writers of the bundle that evaluates a fact, so a source with an extra label needs a context_settings entry that admits it before its facts resolve. Replay treats the three provisioning events as informational.
execbound_harness/context_manifests.py turns a synthetic asset export and a synthetic ticket export (tests/fixtures/context-asset-export.json, context-ticket-export.json) into manifests: ticket states new, in_progress and on_hold map to OPEN, resolved and closed to CLOSED, priorities 1 to 5 to CRITICAL, HIGH, MEDIUM, LOW and LOW, the incident identifier is uuid5(source, "<number>:<asset>") so re-exports update the same rows, and an unmapped state, priority or asset class refuses the export. The export shapes name no real product; adapters for real inventory or ticketing systems are planned, unverified and need separate authorization. The monitoring intake never writes facts or incidents.
Execution, approvals and replay
- Admission. A request with a new idempotency key loads the active bundle, resolves with its settings and freezes the plan with its contract version. Execution describes the same-key retry that resolves with contract defaults.
- Re-attestation. Pending retries, approval decisions and the precommit check re-resolve with the settings of the bundle the execution was frozen under and require an unchanged binding hash. Otherwise they return
STALE_AUTHORIZATION. - Approvals. An approval expires at the earlier of the tenant's approval deadline after creation (120 seconds by default; configurable from 120 to 3,600, or off) and the plan's
valid_until; with the deadline off,valid_untilis the only clock, so an approval never outlives its facts. The Approvals and Activity pages show the contract's display fact as the resource class, add the environment label only when the plan has anenvironmentfact, and render both as escaped text. If a frozen plan names a contract version that the running catalog does not contain, the pages render the escaped labelunknown contract <version>for that row instead of failing the whole listing. - Replay. Replay evaluates recorded frozen plans with the same evaluator and the candidate's settings. A candidate must keep the original mappings and Cedar schema; otherwise its decisions are
UNRESOLVEDwithUNSUPPORTED_CANDIDATE_CONTEXT. See Replay.
Failure codes
| Condition | Raised in | Result |
|---|---|---|
| A mapping names a contract that is missing or does not serve its action and resource type | resolution/registry.py | ResolutionError code INVALID_OPERATION |
| Missing, null, mistyped or out-of-domain fact; inactive, wrong-kind or untrusted source; future or expired observation; missing required incident; setting above the ceiling | resolution/facts.py, execution/snapshot.py | ResolutionError code INVALID_CONTEXT |
| Frozen evidence fails a contract, setting or freshness check, including an unknown contract version | policy/evaluate.py | DENY with reason INVALID_CONTEXT |
| The bundle's mapping reference names a different contract version than the plan | policy/evaluate.py | DENY with reason INVALID_BUNDLE |
| Re-attestation finds a changed binding, earlier observation or shorter validity | execution/snapshot.py | STALE_AUTHORIZATION |
| Mapping contract, contract entities or settings fail at validation, activation or active-bundle load | policy/store.py | PolicyStoreError code INVALID_COMPONENT |
| A replay candidate with the original mappings and schema fails Cedar or contract validation | replay/simulate.py | Candidate DENY with reason POLICY_ERROR |
| A replay candidate fails model validation or names another tenant | replay/simulate.py | ReplayValidationError code INVALID_CANDIDATE |
admit does not pass resolution codes to its caller. It raises a generic ExecutionError and audits the rejection with reason DEPENDENCY_OR_CONTEXT_UNAVAILABLE.
Tests
| File | Covers |
|---|---|
tests/test_contracts.py | CC1: construction rules, production catalog, value matching, effective settings |
tests/test_context_parity.py | CC2: v1 resolution and evaluation against the golden fixture |
tests/test_context_resolution.py | CC3 to CC6: row-backed resolution, claims, unserved contracts, settings, writers, row sources |
tests/test_context_provisioning.py | CP1 to CP12: manifests, the importer's routing, order and version rules, sources, the harness adapter, runtime privileges |
tests/test_context_evaluation.py | CC3, CC5, CC6, CC10: settings during evaluation, frozen validity, synthetic facts in Cedar |
tests/test_context_bundle_validation.py | CC5, CC7: contract entities and settings at validation and activation |
tests/test_context_execution.py | CC3, CC11, CC12: runtime execution, stale approvals, widened and narrowed settings, display fact |
tests/test_resource_facts_schema.py | CC8, CC9: tenant isolation, runtime grants, value bounds, guarded downgrade |
tests/test_replay_scenarios.py | CC10: candidate max age and invalid candidate settings |
tests/context_fixtures.py provides SYNTHETIC, CATALOG, SYNTHETIC_SCHEMA, SYNTHETIC_AUTHORIZATION, synthetic_bundle and seed_synthetic.