Config Reference¶
Cruxible configs are YAML files that define a decision domain: entity types, relationships, named queries, constraints, workflows, providers, artifacts, quality checks, feedback profiles, outcome profiles, and decision policies, plus mutation guards for configured state writes. AI agents generate these configs; Core validates and executes against them.
Top-Level Structure¶
version: "1.0"
name: "my_domain"
description: "Optional description of this decision domain"
# extends: base-config.yaml # release-backed overlay composition (see below)
entity_types: { ... }
relationships: [ ... ]
named_queries: { ... }
constraints: [ ... ]
# Governed workflow sections (all optional)
quality_checks: [ ... ]
feedback_profiles: { ... }
outcome_profiles: { ... }
mutation_guards: [ ... ]
decision_policies: [ ... ]
contracts: { ... }
artifacts: { ... }
providers: { ... }
workflows: { ... }
runtime:
trace_payloads: preview
tests: [ ... ]
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
version |
string | no | "1.0" |
Config schema version |
name |
string | yes | — | Unique name for this domain |
description |
string | no | null |
Human-readable description |
extends |
string | no | null |
Path to a base config for release-backed overlay composition (see Config Composition) |
cruxible_version |
string | no | null |
Version of cruxible-core that produced this config (auto-stamped on save) |
entity_types |
dict | yes* | — | Entity type definitions (*optional when extends is set) |
relationships |
list | no | [] |
Relationship definitions |
named_queries |
dict | no | {} |
Declarative query definitions |
constraints |
list | no | [] |
Validation rules |
quality_checks |
list | no | [] |
Evaluate-time graph quality checks |
feedback_profiles |
dict | no | {} |
Structured feedback vocabularies per relationship type |
outcome_profiles |
dict | no | {} |
Structured outcome vocabularies for trust calibration |
mutation_guards |
list | no | [] |
Reject direct graph mutations unless configured state-side conditions pass |
decision_policies |
list | no | [] |
Action-side behavior rules for queries and workflows |
enums |
dict | no | {} |
Shared enum vocabularies referenced by property schemas |
contracts |
dict | no | {} |
Typed payload contracts for providers/workflows |
artifacts |
dict | no | {} |
Pinned external artifacts referenced by providers |
providers |
dict | no | {} |
Versioned executable leaves used by workflow steps |
workflows |
dict | no | {} |
Declarative step-based execution plans |
runtime |
dict | no | {trace_payloads: preview} |
Local runtime behavior options, including provider trace payload retention |
tests |
list | no | [] |
Fixture-based workflow tests |
Runtime Options¶
runtime controls local execution and audit-capture behavior that is not part
of the state model itself.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
trace_payloads |
string | no | "preview" |
Provider trace payload retention: "full", "preview", or "metadata" |
default_write_policy |
string | no | "direct" |
Instance-wide default direct-write governance: "direct" or "proposal_only". Applies to entity/relationship types whose own write_policy is unset. See Direct-Write Governance. |
Trace payload retention controls what is persisted in provider execution traces:
fullstores full providerinput_payloadandoutput_payloadbodies inline in local SQLite trace rows.previewstores small payload bodies inline, but replaces large payloads with bounded deterministic previews plus digest/byte-count metadata.metadatastores no full provider payload bodies; trace payload fields contain omission placeholders plus digest/byte-count metadata.
Local SQLite does not provide cold storage or later hydration for omitted
payload bodies. Choose full only when local full-body provider provenance is
more important than trace database size.
Direct-Write Governance (refuse_direct_writes)¶
The CRUXIBLE_MODE permission tiers (read_only ⊂ governed_write ⊂
graph_write ⊂ admin) are cumulative: a graph_write actor can both
direct-add a fact and propose one. The tiers therefore cannot express "this
domain is proposal-only" — a per-domain governance axis. refuse_direct_writes
adds that axis.
A type marked proposal_only refuses bare direct graph-write verbs
(add_entity / add_relationship / batch_direct_write / the typed lifecycle
write) and forces state in only through the governed proposal/workflow path. It
is a hard constraint, independent of permission tier — even admin is
refused. The refusal raises DirectWriteRefusedError (HTTP 403,
error_code: direct_write_refused).
An entity type marked mint_only (entity types only — not relationships) is
stricter still: it is writable only by the internal token_mint source and
refuses all other sources, including the governed verbs workflow_apply /
group_resolve. Because a workflow make_entities step would later apply through
workflow_apply and bypass the chokepoint refusal, a config whose make_entities
targets a mint_only entity type is rejected at config load (fail-closed).
Use it for auth-managed identity types: auth-on daemons materialize them from runtime credentials; auth-off daemons materialize a declared local operator identity through the same internal token_mint source.
Scope. refuse_direct_writes governs how state is created — it forces the
direct-write verbs above through the proposal/workflow path. It does not
govern the feedback review channel: promoting an already-staged (pending)
edge to live, or correcting an existing edge, goes through feedback — a
separate path gated by reviewer identity (credential-backed when server auth
is on; attributed to the declared local operator when auth is off). So
proposal_only guarantees that
creation is governed; promotion of a staged edge is exactly as strong as the
feedback review-gate, no stronger.
Three knobs control it:
| Knob | Where | Values | Effect |
|---|---|---|---|
write_policy (per type) |
entity_types.<T> / relationships[] |
direct | proposal_only | mint_only (entity types only) | unset |
Per-type policy. Unset inherits the instance default. An explicit direct opts out of the instance default (but not the env kill-switch). mint_only (entity types only) is stricter than proposal_only: the type is writable only by the internal token_mint source and refuses all other sources, including the governed verbs workflow_apply / group_resolve. A config whose workflow make_entities step targets a mint_only entity type is rejected at load (fail-closed). |
default_write_policy |
runtime |
direct (default) | proposal_only |
Instance-wide default for types whose own write_policy is unset. |
CRUXIBLE_REFUSE_DIRECT_WRITES |
process env (daemon) | truthy (1/true/yes/on) |
Daemon-wide kill-switch for the direct-write verbs: forces proposal_only for every type at the write chokepoint, overriding every per-type opt-out and the default. (Chokepoint only — the feedback review/promotion path is separate; see Scope above.) |
Effective policy (union — any path to proposal_only wins): a write is
refused when the env kill-switch is set OR the type's explicit write_policy
is proposal_only OR the type's write_policy is unset and
runtime.default_write_policy is proposal_only.
CRUXIBLE_REFUSE_DIRECT_WRITES |
type write_policy |
default_write_policy |
Effective |
|---|---|---|---|
| unset | unset | direct |
direct |
| unset | unset | proposal_only |
proposal_only |
| unset | direct |
proposal_only |
direct (opts out) |
| unset | proposal_only |
direct |
proposal_only |
| set | direct |
direct |
proposal_only (env wins) |
| set | unset | direct |
proposal_only |
Always permitted, regardless of policy:
- Relationship writes with
pending: true— they stage an edge for review and are not live. (Entities have no pending path; a direct entity add of aproposal_onlytype is refused outright — add it through a canonicalapply_entitiesworkflow.) - Governed verbs: proposal group resolution (
group propose→ resolve) and canonical workflow apply (apply_entities/apply_relationships).
The default (everything unset) is byte-identical to pre-refuse_direct_writes
behavior — all direct writes succeed.
Config Composition¶
The extends field enables an overlay pattern for release-backed state publishing. A published upstream state model provides entity types, relationships, and workflows; a downstream overlay adds its own internal extensions without duplicating the base.
How it works: cruxible_validate detects extends, resolves the base path relative to the overlay file, composes in memory, and validates the composed result. The raw load_config() function still parses a single file — composition happens in the service/CLI layer. For inline config_yaml (no file path), extends must use an absolute path or validation will error.
At runtime, the release-backed overlay flow (service_reload_config) materializes the composed config to disk as the active config the instance uses.
# overlay config — validated by composing with the base automatically
version: "1.0"
name: kev_triage
extends: ../kev-reference/config.yaml
description: >
Overlay of the KEV reference state for internal vulnerability triage.
entity_types:
Asset:
description: Internal asset from CMDB.
properties:
asset_id: {primary_key: true}
hostname: {indexed: true}
relationships:
- name: asset_owned_by
from: Asset
to: Owner
Composition rules (strict append-only):
| Field category | Fields | Behavior |
|---|---|---|
| Metadata | name, description |
Overlay overrides base |
| Runtime options | runtime |
Overlay runtime options override base runtime options |
| Safe lists | constraints, quality_checks, mutation_guards, tests, decision_policies |
Overlay appends to base |
| Relationships | relationships |
Overlay can only add new names; redefining an upstream relationship raises ConfigError |
| Keyed maps | entity_types, named_queries, enums, feedback_profiles, outcome_profiles, contracts, artifacts, providers, workflows |
Overlay can only add new keys; redefining an upstream key raises ConfigError |
| Other fields | everything else | Overlay can only set if not in base, or if equal to base value |
When extends is set, entity_types may be empty — the base provides them.
entity_types¶
A dict keyed by type name. Each value defines the entity's properties.
entity_types:
Vehicle:
description: "A specific vehicle (year + make + model + trim)"
properties:
vehicle_id: {primary_key: true}
year:
type: int
indexed: true
make: {indexed: true}
model: {indexed: true}
trim: {}
engine: {}
EntityTypeSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
description |
string | no | null |
Human-readable description of this entity type |
properties |
dict | yes | — | Property definitions (see below) |
constraints |
list[string] | no | [] |
Constraint names that apply to this entity type |
write_policy |
string | no | null |
"direct", "proposal_only", or "mint_only". Governs direct entity adds for this type — see Direct-Write Governance. "mint_only" is stricter than "proposal_only": writable only by the internal token_mint source, refusing all other sources including workflow_apply / group_resolve (a config wiring a mint_only type into a workflow make_entities step is rejected at load). null inherits runtime.default_write_policy. |
PropertySchema¶
Each property within an entity type (or relationship) is defined with:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | no for graph properties; yes for contract fields | "string" for graph properties |
Data type: string, int, integer, float, number, bool, date, datetime, json |
primary_key |
bool | no | false |
Mark as the entity's unique identifier |
indexed |
bool | no | false |
Enable fast lookups on this property |
optional |
bool | no | graph properties default to true; contract fields default to false |
Allow null/missing values |
required |
bool | no | null |
Positive alias for optional: false; reject conflicting required/optional values |
default |
any | no | null |
Default value when not provided |
enum |
list[string] | no | null |
Restrict to allowed values |
enum_ref |
string | no | null |
Reference a shared enum defined in top-level enums |
description |
string | no | null |
Human-readable description |
json_schema |
dict | no | null |
JSON Schema documentation for json-typed properties; write-time validation only checks JSON serializability |
Rules:
- Exactly one property per entity type should have primary_key: true.
- primary_key goes on the property, not the entity type.
- Entity and relationship properties default to type: string and optional: true, so field_name: {} is valid shorthand.
- primary_key: true implies required and may not be combined with optional: true.
- Contract fields still require an explicit type and are required by default.
- Use required: true for non-primary-key graph properties that must be present.
- enum and enum_ref are mutually exclusive.
- json_schema is only allowed when type: json. Use it to document the expected structure of complex nested data (e.g., version range arrays).
enums¶
Shared bounded vocabularies referenced by enum_ref. Use these when the same
allowed values appear across multiple entity, relationship, or contract fields.
enums:
asset_status:
description: Lifecycle state for tracked assets.
values: [active, retired, decommissioned]
criticality:
description: Shared rank from lowest to highest.
values: [low, medium, high, critical]
ordered: low_to_high
entity_types:
Asset:
properties:
asset_id: {primary_key: true}
status: {enum_ref: asset_status}
Only an enum declared with ordered: low_to_high (values listed lowest to
highest) may be referenced as an order_by enum_ref in a named query;
referencing an unordered enum there is a config validation error.
Enum values must be non-empty and unique. With extends, overlays may add new
enum names but may not redefine or extend upstream enum vocabularies.
Authoring note — domain
statusvs. entity lifecycle. A domainstatusenum should model progress / workflow states (e.g.planned,active,closed). Entity retirement / deletion is a different axis — "does this entity still exist / is it live" — and is the canonical way to "delete" an entity. It lives in the canonical core entity lifecyclelifecycle.status(uniform across all entities the way relationship lifecycle already is), not a per-kitstatusvalue. When authoring a kit, keep retirement-flavored values (retired,decommissioned,superseded) out of yourstatusenum — theasset_statusexample above mixes the two for illustration, but the canonical soft-delete is the entity lifecycle, not a status value. This keeps "where is this in its workflow" separate from "is this entity still live."
Entity lifecycle.status (read visibility)¶
Every entity carries an optional, typed lifecycle state. It is the typed
lifecycle field of the EntityMetadata envelope (an EntityLifecycleState,
validated on write), the entity analogue of the relationship's
RelationshipMetadata.assertion.lifecycle. It serializes onto the stored entity
metadata under the lifecycle key:
# Stored/serialized shape — NOT hand-authored; written via the typed channel.
metadata:
lifecycle:
status: live # one of: live | superseded | retired (default live)
reason: "replaced by WI-204" # optional
closed_at: "2026-06-23T00:00:00Z" # optional (shared closed_at/closed_by audit pair)
The lifecycle shares its structure with the relationship lifecycle (same
reason, effective window, closed_at/closed_by audit pair, and supersession
links); only the status vocabulary differs (live|superseded|retired
for entities vs active|inactive|superseded|retracted for relationships).
orphaned is not an authorable entity lifecycle status — an orphaned entity
is a derived evaluate/health finding (surfaced as integrity.orphan_entity_count),
not a state you set.
- Default is
live. An entity with nolifecyclemetadata is treated as live, so existing data needs no migration to keep current behavior. - Set it through the typed lifecycle write channel —
entity update --lifecycle-status retired [--lifecycle-reason "…"], orbatch-direct-writewith the typedlifecyclefield on the entity input. The status is validated against the entity lifecycle vocabulary; lifecycle is a typed field, not a free-form metadata blob. Alifecyclekey inside free-formmetadatais not a way to set it — it is carried inertly as ordinary metadata (under the typed envelope's free-form slot) and never changes the entity's lifecycle. There is no reserved metadata key and no special retire verb. - Read gating is uniform. Every read path (
query,list entities, traversal/relationship reads, and the MCP/HTTP equivalents) defaults to live-only: aretired/supersededentity is hidden. The one exception is an explicit by-identity get, which always returns the entity and shows itslifecycle.status(the recovery/inspection path). - The
--stateselector (config fieldrelationship_state) controls visibility:live(default),not-live(only the gated-out set),all(everything). For entities the review-only values (accepted/pending/reviewable) resolve tolive, since an entity has no review axis.
ordered: low_to_high marks a shared enum as semantically ranked. The order of
values is the rank order from lowest to highest. Query order_by clauses can
reference ordered enums with enum_ref to sort by rank instead of lexical string
order; direction: asc means low-to-high and direction: desc means
high-to-low.
relationships¶
A list of relationship definitions connecting entity types.
relationships:
# Deterministic relationship — no proposal policy needed
- name: product_from_vendor
description: Deterministic product-to-vendor mapping from CPE structure.
from: Product
to: Vendor
# Governed judgment relationship — uses proposal_policy + signals
- name: asset_affected_by_vulnerability
description: Accepted judgment that an asset is actually affected.
from: Asset
to: Vulnerability
properties:
installed_version: {}
affected_basis: {}
proposal_policy:
signals:
product_version_evidence:
role: required
always_review_on_unsure: true
scanner_evidence:
role: advisory
RelationshipSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Unique relationship name |
from |
string | yes | — | Source entity type name |
to |
string | yes | — | Target entity type name |
cardinality |
string | no | "many_to_many" |
Cardinality constraint |
properties |
dict | no | {} |
Edge property definitions (same schema as entity properties) |
description |
string | no | null |
Human-readable description |
inverse |
string | no | null |
Name for the reverse traversal direction |
is_hierarchy |
bool | no | false |
Mark as a hierarchical relationship |
proposal_policy |
ProposalPolicyConfig | no | null |
Governed proposal policy (see proposal_policy) |
proposal_identity |
string | no | "thesis_signature" |
"thesis_signature" groups trust by proposal thesis; "relationship_tuple" groups trust by edge tuple and requires proposal_policy |
write_policy |
string | no | null |
"direct" or "proposal_only". Governs direct edge writes for this type — see Direct-Write Governance. null inherits runtime.default_write_policy. |
Notes:
- from and to must reference entity type names defined in entity_types.
- Edge properties use the same PropertySchema as entity properties.
- inverse enables traversing the relationship in reverse by name.
- Relationships with proposal_policy are intended to be governed: edges should be created through the proposal/group resolution flow when they are inferred, classified, or otherwise judgment-bearing. Raw add_relationship calls remain available for explicit deterministic facts.
proposal_policy¶
The proposal_policy block on a relationship defines how candidate group proposals are evaluated and auto-resolved. It connects relationship types to the governed proposal pipeline.
proposal_policy:
signals:
product_version_evidence:
role: required
always_review_on_unsure: true
scanner_evidence:
role: advisory
auto_resolve_when: all_support
auto_resolve_requires_prior_trust: trusted_only
max_group_size: 1000
| Field | Type | Default | Description |
|---|---|---|---|
signals |
dict[str, SignalPolicyConfig] | {} |
Per-signal-source guardrails keyed by the labels emitted from workflow map_signals steps |
auto_resolve_when |
string | "all_support" |
"all_support" or "no_contradict" — when to auto-resolve proposals |
auto_resolve_requires_prior_trust |
string | "trusted_only" |
"trusted_only" or "trusted_or_watch" — trust level required for auto-resolution |
max_group_size |
int | 1000 |
Maximum candidates per group proposal |
SignalPolicyConfig (per signal source within proposal_policy.signals):
| Field | Type | Default | Description |
|---|---|---|---|
role |
string | "required" |
"blocking", "required", or "advisory" — how the signal affects resolution |
always_review_on_unsure |
bool | false |
Force manual review when this signal source returns unsure |
note |
string | "" |
Human-readable note about this signal source's role |
Role semantics:
- blocking: A contradict signal from this source blocks auto-resolution entirely.
- required: The signal is factored into the auto-resolve decision; unsure may trigger review.
- advisory: The signal is recorded but does not affect auto-resolution.
named_queries¶
A dict of declarative query definitions. Every query declares an explicit
mode: traversal starts from an entry entity and walks relationship steps;
collection enumerates one entity type or relationship type directly.
named_queries:
parts_for_vehicle:
mode: traversal
description: "Find all parts that fit a specific vehicle"
entry_point: Vehicle
traversal:
- relationship: fits
direction: incoming
filter:
verified: true
returns: "list[Part]"
compatible_replacements:
mode: traversal
description: "Find replacement parts that also fit the same vehicle"
entry_point: Part
traversal:
- relationship: replaces
direction: both
filter:
direction: [equivalent, upgrade]
- relationship: fits
direction: outgoing
constraint: "target.vehicle_id == $vehicle_id"
returns: "list[Part]"
all_active_fitments:
mode: collection
description: "List live fitment relationships"
result_shape: relationship
returns: fits
where:
edge.properties.verified:
eq: true
NamedQuerySchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mode |
string | yes | — | Query mode: traversal or collection |
description |
string | no | null |
Human-readable description |
entry_point |
string | for traversal | — | Entity type to start a traversal query from; invalid for collection queries |
traversal |
list | for traversal | — | Non-empty sequence of traversal steps; invalid for collection queries |
returns |
string | yes | — | Description of the return type |
result_shape |
string | no | "path" |
Output shape: entity, path, or relationship |
dedupe |
string | no | shape-dependent | Result dedupe mode: entity, path, or none. Entity queries default to entity; path and relationship queries default to path. |
relationship_state |
string | no | "live" |
Read-visibility state: live, accepted, all, not-live, pending, or reviewable. Gates entities by lifecycle and edges by review+lifecycle (see Read visibility below). The runtime/CLI selector for this is the --state flag (state on MCP/HTTP). |
allow_relationship_state_override |
bool | no | false |
Whether runtime callers may override the visibility state |
where |
dict | no | null |
Top-level predicate map for collection queries; invalid for traversal queries |
select |
dict | no | null |
Projection map from output field name to query reference or literal value. When present, user-facing rows return {values} while receipts preserve source evidence for audit and feedback. |
order_by |
list | no | [] |
Deterministic ordering rules. Each item uses by, optional direction (asc or desc), optional value_type (string, int, integer, float, number, bool, date, or datetime), and optional ordered enum_ref. |
include |
dict | no | {} |
One-hop side-context includes keyed by alias. Includes decorate each primary row without advancing traversal or fanning out primary rows. |
limit |
int | no | null |
Query-level output cap applied after traversal, dedupe, path budgets, ordering, and before projection. Result metadata reports pre-limit total_results, effective limit, and limit_truncated. |
max_paths |
int | no | null |
Traversal-time retained-path frontier budget. It caps retained path states for each traversal step, limiting memory and receipt growth. It is not a total candidate-evaluation budget. |
max_paths_per_result |
int | no | null |
Post-traversal final retained-path-per-result cap applied after traversal/dedupe, before ordering and limit. It does not bound traversal work. |
Validation rules:
- mode: collection queries omit entry_point, traversal, include, max_paths, and max_paths_per_result.
- Collection queries support result_shape: entity with returns set to an entity type, or result_shape: relationship with returns set to a canonical relationship name. Reverse aliases are rejected so direction is unambiguous.
- mode: traversal queries require entry_point and at least one traversal step. Put filters on traversal steps, include blocks, or related predicates; top-level where is reserved for collection queries.
Collection where scopes:
- result_shape: entity: paths start with result — e.g. result.properties.status: {in: [active]}.
- result_shape: relationship: edge.properties.<field> filters the edge itself (field names are validated against the relationship's configured schema), and source. / target. filter the endpoint entities — e.g. source.properties.status: {eq: open} keeps only edges whose from-entity is an open incident. entry, current, and candidate are accepted aliases (entry and current resolve to the from-entity, candidate to the to-entity); prefer source/target for readability.
- Traversal queries that intentionally return mixed entity types can set returns: AnyEntity; this skips homogeneous entity-type validation for entity/path rows.
- result_shape: entity requires dedupe: entity.
- For a traversal query with result_shape: entity, a concrete entity returns type, and max_depth on the final step, the engine may traverse through intermediate entity types and collect only the declared return type when at least one relationship in that final step can reach it. This is read-time typed collection, not a virtual or materialized relationship.
- result_shape: relationship requires dedupe: path or none.
- relationship_state: pending requires result_shape: path or relationship, and does not allow dedupe: entity.
- relationship_state: reviewable requires traversal result_shape: path or collection result_shape: relationship, and does not allow dedupe: entity.
- required: false traversal steps are optional continuations, not independent context enrichment. They require result_shape: path or relationship.
- result_shape: relationship may use required: false optional-continuation steps only when the final returned relationship step is still required.
- max_paths and max_paths_per_result require result_shape: path or relationship, and must be positive integers when set.
- max_paths is the retained-path frontier safety control. Once reached for a traversal step, the engine stops retaining/enqueuing more path states and avoids recording traversal receipts for the skipped frontier. Candidates that fail filters before any path is retained can still be evaluated; use a future candidate/work budget if total edge evaluation needs a separate cap.
- max_paths_per_result is a result-time evidence cap. It trims retained paths per final result entity after traversal, when result identity is known. It is distinct from limit: max_paths_per_result controls evidence fanout per result, while limit controls how many ordered rows are returned.
- order_by runs after traversal, dedupe, and path budgets, before limit.
- order_by.value_type and order_by.enum_ref are mutually exclusive.
- order_by.enum_ref must reference a top-level enum with ordered: low_to_high.
- Path budget truncation is reported separately with path_truncated, retained_path_count, and truncation_reasons.
- path_truncated means traversal was cut short by a path budget before the engine could prove completeness. It does not guarantee that every skipped frontier item would have produced a returned row.
- total_path_count is populated only when traversal completes. If traversal-time max_paths cuts exploration short, total_path_count is null because the full possible path count was intentionally not computed.
- Missing projected property or metadata refs resolve to null; missing $input.* refs fail execution.
- Missing $path.<alias>... refs for a non-required traversal alias resolve to null when that step did not match. Unknown aliases still fail validation/execution.
- Missing order values sort last, with stable graph-identity tie-breakers added automatically.
- Query-level limit is part of the named query contract. Runtime/API caller limits are only a caller-facing response cap.
- Projected query receipts retain source path/relationship evidence. User-facing projected results intentionally omit that source payload by default.
- include aliases must not collide with traversal aliases. Include anchors support $entry, $result, $path.<alias>.source, and $path.<alias>.target.
- Includes are one-hop side context. They do not advance the traversal frontier and do not fan out primary rows.
- Traversal queries with result_shape: entity may use includes only with select, so include values are projected explicitly while raw entity rows remain unchanged.
- include.required: true filters out a primary row when that include has no matches. required: false retains the row with exists: false, count: 0, and empty items.
- include.many: false expects at most one match and fails execution if multiple matches are found. Use many: true for repeated side context.
- Include limit is per include per primary row. It sets that include's truncated flag and is separate from query limit, max_paths, and max_paths_per_result.
- Include order_by refs may use $edge, $source, $target, or $input.
Relationship state modes:
- live includes active relationships whose review state is neither pending nor rejected. This includes deterministic/unreviewed state and approved state.
- accepted includes active relationships whose review status is approved.
- pending includes active relationships whose review status is pending.
- reviewable includes live relationships plus pending relationships. Use this for triage/context queries where an agent should see both accepted state and still-reviewable proposals in one evidence path.
Projection refs:
- All shapes: $input.<name>, $entry.entity_type, $entry.entity_id, $entry.properties.<name>, $entry.metadata.<path>, $result.entity_type, $result.entity_id, $result.properties.<name>, $result.metadata.<path>.
- result_shape: path: $path.<alias>.edge.*, $path.<alias>.source.*, and $path.<alias>.target.*. Path refs require a traversal as alias.
- result_shape: relationship: $relationship.*, $from_entity.*, and $to_entity.*.
- Include refs: $include.<alias>.exists, $include.<alias>.count, $include.<alias>.truncated, $include.<alias>.items. Singular includes also support $include.<alias>.edge.*, $include.<alias>.source.*, and $include.<alias>.target.*; many: true includes require selecting items, count, or existence flags.
Projection and ordering example:
named_queries:
remediation_exposure_context:
mode: traversal
entry_point: Vulnerability
returns: Asset
result_shape: path
dedupe: path
traversal:
- as: affected_product
relationship: vulnerability_affects_product
direction: outgoing
- as: exposure
relationship: asset_runs_product
direction: incoming
select:
vulnerability_id: $entry.entity_id
asset_id: $result.entity_id
hostname: $result.properties.hostname
exposure_edge_key: $path.exposure.edge.edge_key
priority: $path.exposure.edge.properties.priority
review_status: $path.exposure.edge.metadata.assertion.review.status
order_by:
- by: $result.properties.criticality
direction: desc
enum_ref: criticality
- by: $path.exposure.edge.properties.priority
direction: desc
enum_ref: criticality
- by: $result.entity_id
direction: asc
max_paths: 500
max_paths_per_result: 20
limit: 50
Include example:
named_queries:
vulnerability_asset_context:
mode: traversal
entry_point: Vulnerability
returns: Asset
result_shape: path
relationship_state: reviewable
traversal:
- as: affected_product
relationship: vulnerability_affects_product
direction: outgoing
- as: installed_product
relationship: asset_runs_product
direction: incoming
include:
exposure:
from: $result
relationship: asset_vulnerability_posture
direction: outgoing
many: true
where:
edge.properties.status:
eq: exposed
owner:
from: $result
relationship: asset_owned_by
direction: outgoing
services:
from: $result
relationship: service_depends_on_asset
direction: incoming
many: true
limit: 10
exceptions:
from: $result
relationship: asset_has_exception
direction: outgoing
many: true
where:
target.properties.status:
in: [active, approved]
This returns the primary vulnerability-to-asset path once per row, with the
configured owner, service, exposure, and exception context attached under the
row's includes map. Include context can also be selected with
$include.<alias>... projection refs.
TraversalStep¶
Each step in the traversal sequence:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
relationship |
string or list[string] | yes | — | Relationship name(s) to traverse. A list fans out across listed types in declared order and merges results. Candidates within each relationship type are stable-sorted when path budgets apply. |
direction |
string | no | "outgoing" |
outgoing, incoming, or both |
filter |
dict | no | null |
Property filters on edges or target entities |
target_filter |
dict | no | null |
Exact-match property filters on candidate entities |
where |
dict | no | null |
Structured traversal predicates. Top-level paths must start with edge, source, target, current, candidate, or entry. |
where_related |
list | no | [] |
Related-edge predicates; at least one matching related edge must exist for each item |
where_not_related |
list | no | [] |
Related-edge predicates; no matching related edge may exist for any item |
constraint |
string | no | null |
Constraint expression to apply during traversal |
constraint_value_type |
string | no | null |
Optional typed constraint comparison: string, int, integer, float, number, bool, date, or datetime |
exclude_if_related |
list | no | [] |
Legacy related-edge exclusion checks |
max_depth |
int | no | 1 |
BFS depth for this step (1 = direct neighbors only). By default, results include entities from depth 1 through max_depth; final-step typed collection on entity-shaped traversal queries may traverse intermediates while emitting only the declared returns type. |
required |
bool | no | true |
Optional continuation. When false, preserves the incoming path if no edge passes relationship state, filters, predicates, related predicates, constraints, and policies. Matching edges still continue to the matched neighbor, which becomes the current $result. |
as |
string | no | null |
Alias for the traversed path segment in path/relationship outputs |
Optional continuation semantics:
required: false makes a traversal step optional, but it does not attach
independent neighbor context to the same result row. When a non-required step
matches, traversal continues to the matched neighbor and that neighbor becomes
the current $result. When no candidate passes relationship state, filters,
predicates, related predicates, constraints, and policies, the incoming path is
preserved and $result remains the prior current entity.
Use required: false for optional successor, replacement, or follow-on paths.
Do not use it when the desired shape is "return this same asset, but attach
owner/service/control facts as additional row context." Use include for that:
it attaches bounded one-hop side context to each primary result row without
changing the traversal result or fanning out rows. Use read tools for ad hoc
context that is not worth baking into the named query contract.
Direction semantics:
- outgoing: Follow edges from entry point (source -> target)
- incoming: Follow edges into entry point (target -> source)
- both: Follow edges in either direction
Structured predicate example:
named_queries:
pending_exposures:
mode: traversal
entry_point: Vulnerability
returns: asset_vulnerability_posture
result_shape: relationship
relationship_state: pending
allow_relationship_state_override: true
traversal:
- relationship: asset_vulnerability_posture
direction: incoming
as: exposure
where:
edge.metadata.assertion.lifecycle.status:
eq: active
target.properties.environment:
eq: production
where_not_related:
- relationship: asset_remediated_vulnerability
direction: outgoing
edge:
properties.verification_status:
eq: verified
target:
entity_id:
eq: $entry.entity_id
Supported structured predicate operators are eq, ne, in, not_in,
lt, lte, gt, gte, exists, contains, and icontains. contains
and icontains require string values; icontains compares case-insensitively.
Predicate values may reference:
$input.<field>$entry.<field>$current.<field>$candidate.<field>$edge.<field>$source.<field>$target.<field>$path.<alias>.edge.<field>$path.<alias>.source.<field>$path.<alias>.target.<field>
Use $path references when filtering an include or predicate against an
already-retained traversal path. Unknown path aliases fail unless the alias
belongs to an absent required: false traversal segment, where the missing
path behaves like a missing value and ordinary predicates fail. $path
references target existing traversal aliases, not include aliases.
include:
remediations:
from: $path.exposure.source
relationship: asset_remediated_vulnerability
direction: outgoing
many: true
where:
target.entity_id:
eq: $path.exposure.target.entity_id
constraints¶
A list of validation rules evaluated during cruxible_evaluate. Constraints check graph state — they flag suspicious or invalid data already in the graph.
constraints:
- name: replacement_same_category
rule: "replaces.FROM.category == replaces.TO.category"
severity: warning
description: "Replacement parts should be in the same category"
ConstraintSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Unique constraint name |
rule |
string | yes | — | Rule expression (see syntax below) |
severity |
string | no | "warning" |
warning or error |
description |
string | no | null |
Human-readable description |
Rule Syntax¶
Constraints compare properties across relationship endpoints:
RELATIONSHIP: The relationship name (e.g.,replaces)FROM: The source entity's propertyTO: The target entity's property<op>: One of==,!=,>,>=,<,<=- Identifiers may contain letters, digits, underscores, and hyphens
Examples:
- replaces.FROM.category == replaces.TO.category — flags any replaces edge where the source and target parts have different categories.
- replaces.FROM.priority > replaces.TO.priority — flags any replaces edge where the source priority does not exceed the target priority.
quality_checks¶
Evaluate-time graph quality checks run during cruxible_evaluate. Six check kinds are available, distinguished by the kind field.
1. property¶
Check a top-level property on entities or relationships.
quality_checks:
- name: cve_id_format
kind: property
severity: error
target: entity
entity_type: Vulnerability
property: cve_id
rule: pattern
pattern: "^CVE-\\d{4}-\\d{4,}$"
| Field | Type | Description |
|---|---|---|
kind |
"property" |
|
target |
"entity" or "relationship" |
What to check |
entity_type |
string | Required when target: entity |
relationship_type |
string | Required when target: relationship |
property |
string | Property name to check |
rule |
string | "required", "non_empty", "type", or "pattern" |
expected_type |
string | Required when rule: type |
pattern |
string | Regex pattern, required when rule: pattern |
2. json_content¶
Check JSON array-of-object content on a json-typed property.
- name: affected_versions_have_useful_keys
kind: json_content
severity: warning
target: relationship
relationship_type: vulnerability_affects_product
property: affected_versions
rule: required_nested_keys
keys: [version_start_including, version_end_excluding, version_exact, fixed_version]
match: any
- name: no_empty_affected_version_objects
kind: json_content
severity: error
target: relationship
relationship_type: vulnerability_affects_product
property: affected_versions
rule: no_empty_objects_in_array
| Field | Type | Description |
|---|---|---|
kind |
"json_content" |
|
target |
"entity" or "relationship" |
What to check |
entity_type / relationship_type |
string | Target type |
property |
string | JSON property name to check |
rule |
string | "no_empty_objects_in_array" or "required_nested_keys" |
keys |
list[string] | Required when rule: required_nested_keys — keys to look for |
match |
string | "any" or "all" — required when rule: required_nested_keys |
3. uniqueness¶
Check entity-property uniqueness, optionally across compound keys.
- name: unique_vendor_product_pair
kind: uniqueness
severity: error
entity_type: Product
properties: [vendor_name, product_name]
| Field | Type | Description |
|---|---|---|
kind |
"uniqueness" |
|
entity_type |
string | Entity type to check |
properties |
list[string] | One or more property names that must be unique together |
4. bounds¶
Check entity or relationship counts against a numeric range.
- name: minimum_products
kind: bounds
severity: warning
target: entity_count
entity_type: Product
min_count: 10
| Field | Type | Description |
|---|---|---|
kind |
"bounds" |
|
target |
"entity_count" or "relationship_count" |
What to count |
entity_type / relationship_type |
string | Target type |
min_count |
int | Optional lower bound |
max_count |
int | Optional upper bound (at least one of min/max required) |
5. cardinality¶
Check per-entity relationship counts in one direction.
- name: products_have_exactly_one_vendor
kind: cardinality
severity: error
entity_type: Product
relationship_type: product_from_vendor
direction: outgoing
min_count: 1
max_count: 1
| Field | Type | Description |
|---|---|---|
kind |
"cardinality" |
|
entity_type |
string | Entity type to check |
relationship_type |
string | Relationship type to count |
direction |
"incoming" or "outgoing" |
Edge direction relative to the entity |
min_count |
int | Optional lower bound |
max_count |
int | Optional upper bound (at least one of min/max required) |
6. relationship_property_consistency¶
Check that an entity property agrees with a related entity reached through a specific relationship. Use this when configs intentionally keep denormalized inspection fields but still need the canonical relationship to stay aligned.
- name: product_vendor_id_matches_vendor_edge
kind: relationship_property_consistency
severity: error
entity_type: Product
relationship_type: product_from_vendor
direction: outgoing
source_property: vendor_id
target_property: vendor_id
allow_missing_source: false
- name: product_vendor_name_matches_vendor_edge
kind: relationship_property_consistency
severity: warning
entity_type: Product
relationship_type: product_from_vendor
direction: outgoing
source_property: vendor_name
target_property: name
allow_missing_source: true
| Field | Type | Description |
|---|---|---|
kind |
"relationship_property_consistency" |
|
entity_type |
string | Source entity type to check |
relationship_type |
string | Relationship connecting source to related entity |
direction |
"incoming" or "outgoing" |
Edge direction relative to the source entity |
source_property |
string | Source entity property to compare |
target_property |
string | Related entity property to compare; omit or use entity_id to compare against the related entity id |
allow_missing_source |
bool | Skip rows where the source property is absent or empty |
Common fields across all quality check kinds:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Unique check name |
kind |
string | yes | — | Check kind discriminator |
description |
string | no | null |
Human-readable description |
severity |
string | no | "warning" |
"warning" or "error" |
feedback_profiles¶
Structured feedback vocabularies scoped to a relationship type. Feedback profiles define the reason codes an agent or human can attach to feedback, and the scope keys that enable grouping and analysis. This is the foundation of Loop 1: feedback drives constraint and decision policy suggestions.
feedback_profiles:
fits:
version: 2
reason_codes:
legacy_unsupported:
description: "Legacy environment is unsupported"
remediation_hint: decision_policy
required_scope_keys: [category, make]
fitment_mismatch:
description: "Part category mismatches vehicle make"
remediation_hint: constraint
required_scope_keys: [category, make]
scope_keys:
category: FROM.category
make: TO.make
FeedbackProfileSchema¶
| Field | Type | Default | Description |
|---|---|---|---|
version |
int | 1 |
Profile version — bump when reason codes or scope keys change semantically |
reason_codes |
dict[str, FeedbackReasonCodeSchema] | {} |
Named reason codes agents can attach to feedback |
scope_keys |
dict[str, FeedbackPathRef] | {} |
Named scope dimensions extracted from graph state at feedback time |
FeedbackReasonCodeSchema¶
| Field | Type | Default | Description |
|---|---|---|---|
description |
string | required | What this reason code means |
remediation_hint |
string | "unknown" |
"constraint", "decision_policy", "quality_check", "provider_fix", or "unknown" — guides analyze_feedback to produce the right kind of suggestion |
required_scope_keys |
list[string] | [] |
Scope keys that must be present when this code is used |
FeedbackPathRef¶
Scope key paths follow the pattern (FROM|TO|EDGE).<property>:
- FROM.category — extracts the category property from the source entity
- TO.make — extracts the make property from the target entity
- EDGE.confidence — extracts the confidence property from the edge
How it works: When an agent submits structured feedback with a reason_code and scope_hints, analyze_feedback groups matching feedback records and produces suggestions:
- Reason codes with remediation_hint: constraint produce constraint suggestions
- Reason codes with remediation_hint: decision_policy produce decision policy suggestions
- Other hints produce quality check or provider fix candidates
outcome_profiles¶
Structured outcome vocabularies for trust calibration and debugging (Loop 2). Outcome profiles define the outcome codes and scope keys attached to recorded outcomes, scoped to either a resolution anchor (proposal outcomes) or a receipt anchor (query/workflow outcomes).
outcome_profiles:
fits_resolution:
anchor_type: resolution
relationship_type: fits
version: 1
outcome_codes:
wrong_match:
description: "The resolved match was incorrect"
remediation_hint: trust_adjustment
required_scope_keys: [category]
stale_data:
description: "Source data was outdated at resolution time"
remediation_hint: provider_fix
scope_keys:
category: RESOLUTION.relationship_type
parts_query:
anchor_type: receipt
surface_type: query
surface_name: parts_for_vehicle
version: 1
outcome_codes:
missing_results:
description: "Expected results were not returned"
remediation_hint: workflow_fix
scope_keys:
query: SURFACE.name
OutcomeProfileSchema¶
| Field | Type | Default | Description |
|---|---|---|---|
anchor_type |
string | required | "resolution" or "receipt" |
version |
int | 1 |
Profile version |
relationship_type |
string | null |
Required for anchor_type: resolution |
workflow_name |
string | null |
Optional for resolution anchors |
surface_type |
string | null |
Required for anchor_type: receipt — "query", "workflow", or "operation". Only "query" and "workflow" are validated and bound to a surface; "operation" is accepted but inert (no binding). |
surface_name |
string | null |
Required for anchor_type: receipt |
outcome_codes |
dict[str, OutcomeCodeSchema] | {} |
Named outcome codes |
scope_keys |
dict[str, OutcomePathRef] | {} |
Named scope dimensions |
OutcomeCodeSchema¶
| Field | Type | Default | Description |
|---|---|---|---|
description |
string | required | What this outcome code means |
remediation_hint |
string | "unknown" |
"trust_adjustment", "require_review", "decision_policy", "provider_fix", "workflow_fix", "graph_fix", or "unknown" |
required_scope_keys |
list[string] | [] |
Scope keys that must be present |
OutcomePathRef¶
Scope key paths depend on anchor type. Valid fields per prefix:
Resolution anchors:
| Prefix | Valid fields |
|---|---|
RESOLUTION |
resolution_id, relationship_type, action, trust_status, resolved_by |
GROUP |
group_signature |
WORKFLOW |
name, receipt_id, trace_ids |
THESIS |
(any thesis_facts key) |
Receipt anchors:
| Prefix | Valid fields |
|---|---|
RECEIPT |
receipt_id, operation_type |
SURFACE |
type, name |
TRACESET |
trace_ids, provider_names, trace_count |
Validation: Resolution profiles require relationship_type and must not set surface_type/surface_name. Receipt profiles require surface_type and surface_name and must not set relationship_type/workflow_name.
mutation_guards¶
Mutation guards reject configured state writes when a configured state-side
condition does not pass. They are enforced on direct entity writes, batch direct
writes, and canonical workflow apply: entity-property guards on entity writes;
relationship-evidence guards (the evidence-requirement condition, scoped to a
relationship type) on relationship writes. Release-backed state pull
re-materialization is exempt, since it replays already-accepted upstream state
rather than authoring new writes.
They are appendable in overlay composition and are not allowed in kind:
ontology configs.
Guards run on writes, not on state reconciliation. The upstream pull-apply on a
release-backed overlay (cruxible state pull) re-materializes the new upstream
release plus the overlay's existing local state and is deliberately guard-exempt:
the local side is a re-materialization of state that already passed its guards
when authored (re-materializing an unchanged value is not a transition and never
fires a guard), the upstream side is governed/published state that this overlay
must not re-litigate, and there is no write actor at merge time for actor-identity
guards to evaluate. The one merge-time risk that is genuinely novel — local edges
dangling onto upstream entities removed in the new release — is enforced
separately as a blocking pull conflict before the merge is materialized.
Entity-property guards fire on any direct write that results in the guarded property value — creating an entity with the value and changing an existing entity to the value are both covered. Updates that re-assert the value an entity already holds are not transitions and do not fire.
Relationship evidence guards fire on writes to the configured relationship type and require the resulting relationship evidence to meet the configured floor. Use them for observation-style relationships whose claims must cite dereferenceable source material. Decision-style relationships should usually declare no evidence floor and rely on ambient attribution: provenance, receipts, actor context, and review history. Every guard field is load-bearing.
The condition is a discriminated union keyed by an explicit condition.type
field — one of query, actor, co_write, or evidence. The type is required;
guard shape is never inferred from which keys are present. The guard-level
operation / effect discriminator fields deliberately do not exist.
mutation_guards:
- name: work_item_closed_requires_review
entity_type: WorkItem
property: status
new_value: closed
condition:
type: query
query_name: approved_review_for_work_item
params:
work_item_id: "$entity.entity_id"
min_count: 1
message: "Work item cannot be closed until approved review exists."
- name: review_request_approval_requires_authorized_actor
entity_type: ReviewRequest
property: status
new_value: approved
condition:
type: actor
allowed_actor_ids: [authorized-reviewer]
message: "ReviewRequest approvals require an authorized actor."
- name: work_item_closed_requires_co_written_review
entity_type: WorkItem
property: status
new_value: closed
condition:
type: co_write
requires:
entity_type: ReviewRequest
via_relationship: review_request_for_work_item
kind: approval # optional: filter the co-written entity's `kind` property
message: "Closing requires a review created in the same write."
- name: finding_support_requires_source_evidence
relationship_type: finding_supports_work_item
condition:
type: evidence
require_evidence: source_evidence
min_count: 1
message: "Observation claims require source evidence."
MutationGuardSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Unique guard name |
entity_type |
string | entity guards | — | Entity type the write applies to |
property |
string | entity guards | — | Property that must be present in the incoming write |
new_value |
any or list | entity guards | — | Guarded resulting value(s) after config property normalization. A scalar guards one value; a list guards several (the guard fires when the write results in any listed value) |
relationship_type |
string | relationship evidence guards | — | Relationship type the write applies to |
condition |
discriminated union on type |
yes | — | Condition that must pass (see types below) |
message |
string | no | null |
Optional user-facing rejection detail |
where |
predicate map | no | null |
Entity-property guards only: scopes the trigger so the guard fires only when the mutated entity matches (candidate scope) |
where_related |
list | no | [] |
Entity-property guards only: related-edge predicates; the guard fires only when every listed edge exists (and matches its inner predicates) on the mutated entity |
where_not_related |
list | no | [] |
Entity-property guards only: related-edge predicates; the guard fires only when no listed edge exists on the mutated entity |
The optional where predicate scopes when an entity-property guard fires. It
uses the same predicate vocabulary as query where (eq/in/not_in/lt/gt/…)
but is restricted to the candidate scope — both predicate paths and any
$-reference operands must start with candidate. and read only the mutated
(proposed) entity. The guard fires only when the proposed entity matches;
otherwise the write is allowed regardless of the condition. where is rejected
on relationship evidence guards.
Guards may also carry where_related / where_not_related (related-edge trigger
scoping, the same RelatedPredicateSpec shape as query traversal steps —
relationship/direction plus per-scope edge/source/target/current/
candidate/entry predicates). They are anchored on the mutated entity and
evaluated against the proposed graph at live visibility (the canonical visible
state; there is no per-guard visibility knob). The guard fires only when every
where_related edge exists (and matches its inner predicates) and no
where_not_related edge exists. Like where, they are entity-property guards
only and are rejected on relationship evidence guards.
The condition.type discriminator selects the condition variant:
type |
Condition | Applies to |
|---|---|---|
query |
NamedQueryResultCountGuardCondition | entity guards |
actor |
ActorIdentityGuardCondition | entity guards |
co_write |
CoWriteGuardCondition | entity guards |
evidence |
EvidenceRequirementGuardCondition | relationship guards |
NamedQueryResultCountGuardCondition (type: query)¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
query |
yes | — | Condition discriminator |
query_name |
string | yes | — | Named query to execute against the proposed graph state |
params |
dict | no | {} |
Query params; values may reference write context |
min_count |
int | conditional | null |
Minimum result count; at least one of min_count or max_count is required |
max_count |
int | conditional | null |
Maximum result count; at least one of min_count or max_count is required |
Supported param references:
$entity.entity_type$entity.entity_id$entity.properties.<name>$current.properties.<name>$new_value$old_value
On entity creation there is no prior state: $old_value resolves to null,
and $current.properties.<name> cannot resolve, so a guard using $current
refs fails closed on creation. Prior-state refs therefore make a guard
transition-only in practice.
ActorIdentityGuardCondition (type: actor)¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
actor |
yes | — | Condition discriminator |
allowed_actor_ids |
list[string] | yes | — | Actor ids allowed to perform the guarded transition |
Actor identity conditions compare the current write's
GovernedActorContext.actor_id to allowed_actor_ids. Missing actor context
fails the guard. This condition is useful for guarded approval transitions where
the authority comes from authenticated runtime credential identity or a Cloud
control-plane supplied actor context.
CoWriteGuardCondition (type: co_write)¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
co_write |
yes | — | Condition discriminator |
requires |
CoWriteRequirement | yes | — | The entity that must be co-created in the same write |
CoWriteRequirement¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type |
string | yes | — | Entity type that THIS write must create |
via_relationship |
string | yes | — | Relationship that must link the created entity to the guarded $entity |
kind |
string | no | null |
When set, the co-written entity's kind property must equal this value |
Co-write conditions pass only when the current write delta both creates an
entity of requires.entity_type (optionally kind-filtered) AND creates a
requires.via_relationship edge linking it to the guarded $entity. "Created in
THIS write" means present in the write delta — a stale pre-existing linked entity
or a pre-existing edge does not satisfy the requirement; the entity and its
linking edge must both be new in this write. The required edge may attach the
co-written entity to $entity in either direction; the relationship's configured
endpoints determine the valid direction. Because the entity and edge must arrive
together, co-write conditions are satisfiable through the batch direct-write path
(which writes entities and edges in one delta) but not through entity-only writes
or step-by-step workflow apply, where they fail closed.
EvidenceRequirementGuardCondition (type: evidence)¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
evidence |
yes | — | Condition discriminator |
require_evidence |
source_evidence |
yes | — | Require dereferenceable source-evidence refs resolved from registered source artifacts |
min_count |
int | no | 1 |
Minimum number of source-evidence refs required; must be at least 1 |
Evidence requirement guards are relationship-scoped: they require
relationship_type and must not define entity_type, property, or
new_value. The guard counts resolved source_evidence locators, not free-text
evidence_rationale alone. Generic evidence_refs only satisfy the floor when
they are dereferenceable source_artifact refs with chunk identity and content
hash metadata, as produced by source artifact registration.
For batch direct writes, guards evaluate against the proposed batch graph, so valid same-batch entities and relationships can satisfy the named query before anything is committed.
Dry-run (and real) added/updated counts cover explicit writes only —
when derived relationships ship, derived-edge effects will be reported in a
separate additive field, never folded into the write counts. Guard conditions
already see query-time derived edges in dry-runs by construction, since guards
evaluate the named-query engine against the proposed graph.
decision_policies¶
Action-side behavior rules applied during query execution or workflow proposal. Decision policies are the action controls that complement state-side constraints. While constraints flag bad data in the graph, decision policies change what queries return or what workflows propose.
decision_policies:
- name: suppress_legacy_honda_brakes
description: "Don't return legacy brake parts for Honda vehicles"
applies_to: query
query_name: parts_for_vehicle
relationship_type: fits
effect: suppress
match:
from:
category: brakes
to:
make: Honda
rationale: "Legacy brake fitments for Honda are unreliable — see feedback batch 2026-03"
- name: review_substitutes_plant_b
description: "Require manual review for substitute proposals at Plant B"
applies_to: workflow
workflow_name: propose_substitutes
relationship_type: safe_to_substitute
effect: require_review
match:
context:
scope_plant_id: PLANT-B
expires_at: "2026-06-30"
DecisionPolicySchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Unique policy name |
description |
string | no | null |
Human-readable description |
rationale |
string | no | "" |
Why this policy exists (reference to feedback, incident, etc.) |
applies_to |
string | yes | — | "query" or "workflow" |
query_name |
string | conditional | null |
Required when applies_to: query |
workflow_name |
string | conditional | null |
Required when applies_to: workflow |
relationship_type |
string | yes | — | Relationship type this policy applies to |
effect |
string | yes | — | "suppress" (query only) or "require_review" |
match |
DecisionPolicyMatch | no | {} |
Exact-match selectors (see below) |
expires_at |
string | no | null |
Optional expiry date (ISO 8601) |
DecisionPolicyMatch¶
| Field | Type | Default | Description |
|---|---|---|---|
from |
dict | {} |
Exact-match on source entity properties |
to |
dict | {} |
Exact-match on target entity properties |
edge |
dict | {} |
Exact-match on edge properties |
context |
dict | {} |
Exact-match on workflow context (e.g., scope keys) |
Validation:
- Query policies require query_name and only support effect: suppress.
- Workflow policies require workflow_name and support both effects.
Keep the distinction clean:
- Constraints = suspicious or invalid graph state (evaluated by cruxible_evaluate)
- Decision policies = query/workflow behavior changes (enforced at execution time)
contracts¶
Typed payload contracts for provider inputs and outputs. Contracts define the fields a provider expects to receive and the shape of what it returns.
Common plumbing contracts are built in and do not need to be declared:
cruxible.EmptyInput: no input fields and no extras.cruxible.JsonObject: any JSON-serializable object payload.cruxible.JsonItems:{items: <json>}.cruxible.ParsedTabularBundle:{artifact, tables, files, diagnostics}from the common tabular parser.
contracts:
PublicKevRows:
description: "Rows of joined KEV + NVD + EPSS data"
fields:
items:
type: json
json_schema:
type: array
items:
type: object
properties:
cve_id: {type: string}
vendor_id: {type: string}
product_id: {type: string}
cvss_score: {type: number}
ContractSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
description |
string | no | null |
Human-readable description |
fields |
dict[str, PropertySchema] | yes | — | Field definitions. Contract fields must define type explicitly and are required by default. |
allow_extra |
bool | no | false |
Allow undeclared JSON-serializable fields; used by cruxible.JsonObject |
artifacts¶
Pinned external artifacts referenced by providers. Artifacts represent data bundles, models, or other resources that providers depend on. The digest hash enables reproducible builds — the workflow lock verifies the live artifact matches the hash at lock time.
artifacts:
public_kev_bundle:
kind: directory
uri: ./data
digest: sha256:f884e5f8fad66c6bba54face97863137833ab26035d7a4cda333063d0ab224f9
ProviderArtifactSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
kind |
string | yes | — | Artifact kind (e.g., directory, file, model) |
uri |
string | yes | — | Location (relative path, URL, etc.) |
digest |
string | no | null |
Content hash for reproducibility verification (sha256:-prefixed) |
metadata |
dict | no | {} |
Arbitrary metadata |
providers¶
Versioned executable leaves used by workflow steps. A provider is a callable that takes a typed input, produces a typed output, and generates an execution trace for the receipt chain.
providers:
parse_public_kev_bundle:
kind: function
description: >
Parse the pinned public KEV artifact into generic tables.
contract_in: cruxible.JsonObject
contract_out: cruxible.ParsedTabularBundle
ref: cruxible_core.providers.common.tabular.load_tabular_artifact_bundle
version: "1.0.0"
deterministic: true
runtime: python
artifact: public_kev_bundle
normalize_public_kev_reference:
kind: function
description: >
Normalize explicit KEV, EPSS, and NVD rows selected by workflow config.
contract_in: PublicKevReferenceInput
contract_out: cruxible.JsonItems
ref: providers.normalize_public_kev_reference
version: "1.0.0"
deterministic: true
runtime: python
ProviderSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
kind |
string | yes | — | "function", "model", or "tool" |
description |
string | no | null |
What this provider does |
contract_in |
string or inline ContractSchema | yes | — | Input contract reference. May be config-defined, built-in (cruxible.*), or an inline contract object. |
contract_out |
string or inline ContractSchema | yes | — | Output contract reference. May be config-defined, built-in (cruxible.*), or an inline contract object. |
ref |
string | yes | — | Callable reference (e.g., module.function_name) |
version |
string | yes | — | Semantic version for lock-file reproducibility |
deterministic |
bool | no | true |
Whether the provider produces identical output for identical input |
artifact |
string | no | null |
Name of artifact this provider depends on (must exist in artifacts) |
runtime |
string | no | "python" |
Execution runtime: "python", "http_json", or "command" |
side_effects |
bool | no | false |
Whether the provider has side effects |
config |
dict | no | {} |
Provider-specific configuration |
Only providers whose job is to load or parse a source artifact should declare
artifact. Domain transform providers should receive the least information
they need through explicit workflow input fields. Required table selection and
source-table mapping belong in workflow config:
- id: raw_tables
provider: parse_public_kev_bundle
input:
expected_tables:
- known_exploited_vulnerabilities
as: raw_tables
- id: rows
provider: normalize_public_kev_reference
input:
kev_rows: $steps.raw_tables.tables.known_exploited_vulnerabilities.rows
as: rows
workflows¶
Declarative step-based execution plans. Workflows compose queries, providers, and graph mutations into reproducible pipelines. A workflow type declares whether it is utility, canonical, proposal, or decision_support.
workflows:
build_public_kev_reference:
type: canonical
description: >
Build the canonical public KEV reference layer from bundled data.
contract_in: cruxible.EmptyInput
steps:
- id: raw_tables
provider: parse_public_kev_bundle
input:
expected_tables:
- known_exploited_vulnerabilities
as: raw_tables
- id: rows
provider: normalize_public_kev_reference
input:
kev_rows: $steps.raw_tables.tables.known_exploited_vulnerabilities.rows
as: rows
- id: vendors
make_entities:
entity_type: Vendor
items: $steps.rows.items
entity_id: $item.vendor_id
properties:
vendor_id: $item.vendor_id
name: $item.vendor_name
as: vendors
- id: product_vendor
make_relationships:
relationship_type: product_from_vendor
items: $steps.rows.items
from_type: Product
from_id: $item.product_id
to_type: Vendor
to_id: $item.vendor_id
as: product_vendor
- id: apply_vendors
apply_entities:
entities_from: vendors
as: apply_vendors
- id: apply_product_vendor
apply_relationships:
relationships_from: product_vendor
as: apply_product_vendor
returns: apply_product_vendor
WorkflowSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
description |
string | no | null |
What this workflow does |
type |
utility, canonical, proposal, or decision_support |
no | utility |
Workflow contract for execution and agent-facing lifecycle |
contract_in |
string or inline ContractSchema | yes | — | Workflow input contract reference. May be config-defined, built-in (cruxible.*), or an inline contract object. |
contract_out |
string or inline ContractSchema | no | null |
Optional final output contract reference. Validates only the payload selected by returns after all workflow steps have run. |
steps |
list[WorkflowStepSchema] | yes | — | Ordered list of steps |
returns |
string | yes | — | ID of the step whose output is the workflow result |
contract_out is an agent-facing boundary check over the assembled workflow
output. It does not validate every provider, query, transform, or intermediate
step output; those steps keep their own validation rules. Omit contract_out to
preserve the current unvalidated final-output behavior.
Workflow Step Types¶
Each step must define exactly one of these operations:
| Step type | Purpose | Key fields |
|---|---|---|
provider |
Call a registered provider | provider, input, as |
query |
Run a named query or inline query definition | query, params?, relationship_state?, as |
assert |
Guard condition — fail the workflow if not met | assert: {left, op, right, message} |
assert_not_truncated |
Guard that read/query context was not truncated | assert_not_truncated: {step} |
assert_count |
Guard a read/result collection count | assert_count: {step, count, op, value} |
assert_exists |
Guard that one intermediate reference resolves to a present value | assert_exists: {ref, message?} |
shape_items |
Project, rename, require, and cast list-shaped rows | shape_items: {items, include_input?, rename?, fields?, casts?, required?}, as |
join_items |
Indexed inner join over two item sets | join_items: {left_items, right_items, left_key, right_key, fields}, as |
filter_items |
Filter rows with exact filters and comparisons | filter_items: {items, where?, comparisons?}, as |
aggregate_items |
Deterministically summarize rows with grouped measures | aggregate_items: {items, group_by?, measures}, as |
dedupe_items |
Deterministically deduplicate rows | dedupe_items: {items, keys, strategy?, rank?}, as |
make_entities |
Build an entity set from list data | make_entities: {entity_type, items, entity_id, properties}, as |
make_relationships |
Build a relationship set from list data | make_relationships: {relationship_type, items, from_type, from_id, to_type, to_id, properties, evidence?}, as |
apply_entities |
Apply a built entity set to graph state | apply_entities: {entities_from}, as |
apply_relationships |
Apply a built relationship set to graph state | apply_relationships: {relationships_from}, as |
apply_all |
Apply explicit entity sets, then relationship sets | apply_all: {entities_from, relationships_from}, as |
register_source_artifacts |
Register source artifacts from workflow row data (canonical only; content is workflow data, never files or URLs; identical-content re-runs noop, conflicting content errors) | register_source_artifacts: {items, artifact_id, content, kind, label?, original_uri?, retention?}, as |
make_candidates |
Build relationship candidates for governed proposals | make_candidates: {relationship_type, items, from_type, from_id, to_type, to_id, properties, evidence?}, as |
map_signals |
Convert provider output to tri-state signal-source evidence | map_signals: {signal_source, items, from_id, to_id, evidence?, evidence_refs?, score/enum}, as |
propose_relationship_group |
Assemble a governed group proposal from candidates + signals | propose_relationship_group: {relationship_type, candidates_from, signals_from, on_empty?}, as |
Step Reference Syntax¶
Steps reference data from prior steps and the current item in list iterations:
| Reference | Meaning |
|---|---|
$input |
Workflow input payload |
$steps.<step_id> |
Output of a prior step (by its as alias) |
$steps.<step_id>.<field> |
A specific field from a prior step's output |
$item |
Current item when iterating over a list (used inside make_* and map_signals) |
$item.<field> |
A specific field on the current item |
Use evidence on make_candidates/make_relationships and
evidence_refs on map_signals for provenance pointers that should follow a
proposal or deterministic relationship into relationship.metadata.evidence.
Keep relationship properties for domain facts such as basis, status, version,
or scope fields.
Read-step outputs:
- query returns {results: [...], total_results, returned_results, ...} using the same result rows and metadata as the query engine.
- shape_items returns {items, input_count, output_count, dropped_count, drop_examples}.
- join_items returns {items, left_count, right_count, skipped_right_count, matched_left_count, output_count}.
- filter_items returns {items, input_count, output_count, filtered_count}.
- aggregate_items returns {items, input_count, group_count, output_count}.
- dedupe_items returns {items, input_count, output_count, duplicate_count, duplicate_examples}.
Read steps also expose consistent completeness metadata: total_results,
returned_results, limit, truncated, limit_truncated, path_truncated,
and truncation_reasons. Query steps additionally expose result_shape,
dedupe, relationship_state, policy_summary, and the child query
receipt_id. Transform steps that consume read output preserve that metadata in
source_metadata.
Workflow graph reads are query-engine-backed. Reusable product/API surfaces
should normally be named queries. Workflow-local collection reads can use an
inline mode: collection query definition:
- id: production_assets
query:
mode: collection
result_shape: entity
returns: Asset
where:
result.properties.environment:
eq: production
order_by:
- by: $result.entity_id
direction: asc
as: production_assets
- id: accepted_asset_products
query:
mode: collection
result_shape: relationship
returns: asset_runs_product
relationship_state: accepted
as: asset_products
Collection queries omit entry_point and do not define
traversal. result_shape: entity enumerates entities of returns;
result_shape: relationship enumerates relationships of returns using the
query engine's relationship-state semantics. result_shape: path is invalid
for collection queries. Downstream steps consume query rows through
$steps.<alias>.results, not items.
Older workflow-specific list_entities and list_relationships read steps
are not supported. Moving collection reads into query keeps filtering,
ordering, relationship visibility, receipts, truncation metadata, and query
evidence in one engine instead of duplicating graph-read semantics in
workflow code.
Guarding Partial Read Context¶
Agent-facing workflows should fail explicitly when a limited or path-budgeted
read would make the output incomplete. Use assert_not_truncated and
assert_count for common completeness checks:
workflows:
exposure_context:
contract_in: ExposureInput
contract_out: ExposureContext
steps:
- id: exposed_assets
query: exposed_assets_for_vulnerability
params:
vulnerability_id: $input.vulnerability_id
as: exposed_assets
- id: require_complete_exposure_context
assert_not_truncated:
step: exposed_assets
- id: require_some_exposures
assert_count:
step: exposed_assets
count: returned_results
op: gt
value: 0
returns: exposed_assets
The same pattern works after shaping or filtering because transforms preserve read metadata:
- id: shaped_exposures
shape_items:
items: $steps.exposed_assets.results
fields:
asset_id: $item.values.asset_id
priority: $item.values.priority
as: shaped_exposures
- id: require_complete_shaped_context
assert_not_truncated:
step: shaped_exposures
Use assert_exists for required intermediate context refs where a missing nested
path should produce an author-controlled message instead of a low-level
reference-resolution error:
- id: require_first_asset_id
assert_exists:
ref: $steps.exposures.results[0].values.asset_id
message: first exposure must include an asset id
assert_count.count supports returned_results, total_results, items, and
results. assert_exists treats null and empty strings as missing; false,
0, empty lists, and empty objects are present values. General assert remains
available for arbitrary comparisons and is equivalent to the longer explicit
forms of these common checks.
contract_out validates the final output shape selected by returns. Read
metadata guards validate whether the workflow had complete enough source
context to support that output.
Dataflow Steps¶
Use dataflow steps for deterministic row mechanics that should be visible in the workflow receipt rather than hidden inside a provider.
shape_items applies operations in this order: rename -> fields -> casts ->
required. Rename keys are top-level only. fields resolves against the
post-rename item and may overwrite projected keys. Casts are explicit and
support str, int, float, bool, and json; missing and null values are
left for required handling.
join_items currently supports join_type: inner. It indexes the right side by
the canonical JSON form of right_key, skips right rows with null keys, and
preserves left-row order with right-match order for one-to-many fanout.
filter_items uses exact-match/list-membership where rules plus comparison
predicates. where reads top-level item keys and may use literals or $input.*
refs only. Comparisons may use normal workflow refs, including $item,
$input, and prior $steps.
aggregate_items groups already-materialized rows and computes deterministic
summary rows. Omit group_by for one global aggregate row; global aggregates
return one row even when the input is empty, so downstream steps can rely on a
stable summary object. Supported measures are count, count_where,
count_distinct, sum, min, and max. count_distinct ignores null
values and uses canonical JSON identity for structured values. sum, min,
and max can declare value_type (number, date, datetime, etc.) to use
the shared typed comparison/coercion rules. Aggregation preserves source
truncation metadata when it summarizes read/query-derived rows.
Grouped count example:
- id: exposure_counts
aggregate_items:
items: $steps.exposures.results
group_by:
priority: $item.values.priority
measures:
exposure_count:
count: true
affected_assets:
count_distinct:
value: $item.values.asset_id
critical_count:
count_where:
left: $item.values.priority
op: eq
right: critical
as: exposure_counts
Global count example:
- id: exposure_total
aggregate_items:
items: $steps.exposures.results
measures:
exposure_count:
count: true
as: exposure_total
dedupe_items requires one or more keys and supports first, last, max,
and min. Ranked strategies require rank; missing ranks lose to present
ranks, and ties keep the earlier item.
apply_all¶
apply_all is a canonical workflow step for reducing repetitive apply
boilerplate while keeping writes explicit. It applies entity sets first in the
listed order, then relationship sets in the listed order, reusing the same
validation and write semantics as apply_entities and apply_relationships.
It does not infer "all previous steps"; every source alias must be listed.
- id: apply_local_state
apply_all:
entities_from:
- assets
- owners
- controls
relationships_from:
- owned_by_edges
- control_edges
as: apply_local_state
The output contains entity_results, relationship_results, top-level
create_count, update_count, noop_count, and duplicate-input totals.
Common providers and step types have different jobs: step types are engine-owned deterministic workflow/dataflow mechanics, while common providers remain reusable but opaque adapters, external services, model calls, or domain-policy modules.
Governed Proposal Steps¶
For type: proposal workflows that produce governed proposals (fuzzy matching, judgment calls), the three-step pattern is:
make_candidates— build candidate (from, to) pairs with propertiesmap_signals— convert provider scores/enums to tri-state signals per signal sourcepropose_relationship_group— assemble candidates + signals into a group proposal
The group then enters the resolution lifecycle (auto-resolve or manual review) based on the relationship's proposal_policy config.
Workflow proposal signatures are Cruxible-generated. Config authors provide
thesis_text for human explanation and analysis_state for review/debug
context, but workflow propose_relationship_group steps do not author
thesis_facts. Cruxible builds the stored signature facts from executable
structure: workflow name, proposal step id, relationship shape, candidate
alias, actual consumed signal batches, the relationship proposal policy, and a
Cruxible-controlled proposal logic digest. thesis_text, analysis_state, and
suggested_priority are not hashed.
Direct agent-authored group proposals may provide optional caller
thesis_facts as signature scope. Cruxible stores that scope under
agent_scope in generated thesis_facts; generated top-level fields such as
origin, relationship, and signals remain Cruxible-owned. The origin is
marked agent with evidence_mode: agent_supplied, and signal-source facts
come from member signals supplied on the proposal. Agent-supplied facts cannot
impersonate workflow/provider-backed evidence; use a configured workflow when
evidence must be provider-backed.
propose_relationship_group is strict by default: if candidates_from resolves
to an empty candidate set, the workflow fails. Set on_empty: complete only when
"no candidates" is a valid terminal outcome for that workflow. In that case no
candidate group is created, the workflow succeeds with status: no_candidates,
and the workflow receipt records group_created: false.
- id: proposal
propose_relationship_group:
relationship_type: asset_remediated_vulnerability
candidates_from: candidates
signals_from: [remediation_signals]
on_empty: complete
thesis_text: Close stale exposure edges
as: proposal
map_signals mapping modes (exactly one required):
-
Thescore: Map a numeric value to signals using thresholdspathis a field name on each item — the executor prepends$item.automatically, so writesimilarity_scorenot$item.similarity_score. Values >=support_gteproducesupport, >=unsure_gteproduceunsure, below producecontradict. -
enum: Map string values to signals using a lookup table
tests¶
Fixture-based workflow tests defined in the config. These are run by cruxible test to verify workflow behavior.
tests:
- name: kev_reference_builds
workflow: build_public_kev_reference
input: {}
expect:
receipt_contains_provider: parse_public_kev_bundle
WorkflowTestSchema¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Test name |
workflow |
string | yes | — | Workflow to execute (must exist in workflows) |
input |
dict | no | {} |
Input payload for the workflow |
expect |
WorkflowTestExpectSchema | no | {} |
Assertions on the result |
WorkflowTestExpectSchema¶
| Field | Type | Description |
|---|---|---|
output_equals |
any | Exact match on the workflow output |
output_contains |
dict | Subset match on the workflow output |
receipt_contains_provider |
string or list[string] | Provider name(s) that must appear in the execution receipt |
error_contains |
string | Expected error substring (for negative tests) |
Full Example¶
The KEV triage overlay config (kits/kev-triage/config.yaml) demonstrates a release-backed overlay that extends a reference layer with governed judgment relationships. Note: This config requires composition with its base (kits/kev-reference/config.yaml) before it can be validated or loaded — Vulnerability, Product, and other reference types are defined in the base, not here:
version: "1.0"
name: kev_triage
extends: ../kev-reference/config.yaml
description: >
Overlay of the KEV reference state for internal vulnerability triage.
entity_types:
Asset:
description: Internal asset from CMDB, cloud inventory, or endpoint tooling.
properties:
asset_id: {primary_key: true}
hostname: {indexed: true}
criticality: {}
environment: {}
internet_exposed: {type: bool}
Owner:
description: Team or person responsible for an asset.
properties:
owner_id: {primary_key: true}
name: {}
team: {}
relationships:
- name: asset_owned_by
description: Ownership mapping for assets.
from: Asset
to: Owner
- name: asset_affected_by_vulnerability
description: Accepted judgment that an asset is affected by a vulnerability.
from: Asset
to: Vulnerability
properties:
installed_version: {}
affected_basis: {}
proposal_policy:
signals:
product_version_evidence:
role: required
always_review_on_unsure: true
scanner_evidence:
role: advisory
named_queries:
affected_assets_for_vulnerability:
mode: traversal
description: Find internal assets accepted as affected by a vulnerability.
entry_point: Vulnerability
returns: Asset
traversal:
- relationship: asset_affected_by_vulnerability
direction: incoming
owner_patch_queue:
mode: traversal
description: Find vulnerabilities affecting an owner's assets.
entry_point: Owner
returns: Vulnerability
traversal:
- relationship: asset_owned_by
direction: incoming
- relationship: asset_affected_by_vulnerability
direction: outgoing
# Operational configs load local state through workflows with providers,
# dataflow steps, make_entities/make_relationships, and apply_* steps.
See also the reference layer config (kits/kev-reference/config.yaml) for a complete example with workflows, providers, contracts, artifacts, and quality checks. Relationship-level proposal_policy.signals defines governed proposal policy.