Prompt Inspection & Bundle Versions
A security decision prompt runs as a versioned prompt bundle. This page covers how a bundle is identified, how the bundle of a request is chosen, how a bundle version moves through the registry states behind the release gate, how shadow runs, canaries and rollbacks work, and how the prompt inspector checks the sealed prompts. For the prompt itself, see Security Decision Prompt Pipeline.
Prompt bundles
A bundle is a governance descriptor (its prompt key, template key, prompt version, contract version, release status and rollback version, among others) together with the user prompt layout its code draws. Its prompt version is the bundle version and names its manifest on the classpath: prompt-bundles/manifests/<promptKey>/<version>.json. A build runs exactly two bundles of cortex.security-decision: the build default (v19 sections) and the v20 bundle (dictionary blocks). Older manifests stay as records of earlier bundles; they cannot be run.
- Element states —
PRESENThashes the element content;ABSENT,UNVERSIONEDandINCLUDEDhash a fixed marker, so the state itself is part of the identity. The bundle hash covers every element. The v19 bundle, for example, marks B2 (no dictionary) and B5 (no minimum action rules) ABSENT. - Checked on read — a manifest whose bundle hash does not match its elements, or two manifests of one name with different hashes, fail fast.
- Manifest fields — besides the elements a manifest names its golden inputs, parent version, vocabulary version and compatible vocabulary versions, its rollback targets and its declared model profiles.
Choosing the bundle of a request
One selector chooses the bundle each request runs, picked by contexa.security.tiered.prompt-bundle.selector. The first prompt of an event fixes the bundle on the event, and its later prompts (Layer2 or a second pass) reuse it. A bundle version this build does not run falls back to the build default, reported once per version.
| Selection source | When |
|---|---|
BUILD_DEFAULT | Nothing chose a bundle, the selector failed, or the chosen version is not one this build runs. |
CONFIGURATION | selector=configuration (default): the version in contexa.security.tiered.prompt-bundle.active, the same for every request. |
REGISTRY | selector=registry: the latest ACTIVE release of the request's scope, or of PLATFORM_GLOBAL when the scope has none. |
CANARY | selector=registry: the scope's CANARY release, for a request inside its share and targets. |
SHADOW | The scope's SHADOW release, run on sampled copies apart from the decision (see shadow runs). |
Registry states
PromptBundleState is the one lifecycle vocabulary of the registry. Older stored values are read through it, so gates, queries and screens classify a release the same way. Only ACTIVE and CANARY are enforced at runtime.
The small label under each state is the older release status name. The transition API takes either name and reads it through PromptBundleState; its receipt and the screens name the state canonically.
Moving a release to ACTIVE deprecates the other ACTIVE releases of its scope, prompt key and template, so one release is active. A release moves to DEPRECATED only while another release of its scope is active (NO_OTHER_ACTIVE_RELEASE).
| Current state | Allowed next states |
|---|---|
AVAILABLE | SHADOW, RETIRED |
SHADOW | CANARY, RETIRED |
CANARY | ACTIVE, DEPRECATED, RETIRED |
ACTIVE | CANARY, DEPRECATED, RETIRED |
DEPRECATED | CANARY, RETIRED |
RETIRED | None |
PLATFORM_GLOBAL scope. A new row takes its descriptor's status, so the v19 bundle starts ACTIVE and the v20 draft starts AVAILABLE. A row declared ACTIVE starts AVAILABLE instead when the scope already has an ACTIVE release of a bundle this build runs, so a code sync never takes the default selection from a release the gate activated; an ACTIVE row of a bundle this build no longer runs does not hold the new build default back. Later syncs create missing rows and refresh code-owned content, never the operational fields.
Release gate
Every transition goes through PromptGovernanceReleaseGateService. A transition that is not allowed from the current state is blocked with ILLEGAL_STATE_TRANSITION; otherwise the gate checks the evidence the target state needs. A blocked transition changes nothing and is recorded with its blockers; an applied one sets the registry status, records the approval and rollback state, and tells the runtime caches.
| Target | Blocked when |
|---|---|
SHADOW | CHANGE_LEDGER_MISSING — no change record exists |
CANARY | EVALUATION_NOT_FOR_BUNDLE_HASH, CHANGE_LEDGER_MISSING, MODEL_COMPATIBILITY_MISSING, BUDGET_PROFILE_MISSING, ROLLBACK_TARGET_MISSING |
ACTIVE | The CANARY blockers, plus PROMPT_INJECTION_GATE_FAILED, NON_FUNCTIONAL_GATE_FAILED (latency or cost), EMPIRICAL_EVAL_MISSING, TEVV_GATE_NOT_READY (the evaluation covers the scenario families the bundle manifest declares in evaluationScenarioFamilies, or by default PROMPT_INJECTION, TOOL_MISUSE and MEMORY_POISONING), TOKEN_COUNTING_POLICY_MISSING, CACHE_POLICY_MISSING, COMPACTION_POLICY_MISSING |
DEPRECATED | ROLLBACK_TARGET_MISSING |
EVALUATION_NOT_FOR_BUNDLE_HASH means the evaluation on record was not made with this bundle's hash: the evidence of a bundle is the evaluation of that bundle.
POST /contexa/admin/api/prompt-quality/releases/transition
Content-Type: application/json
{
"registryScope": "PLATFORM_GLOBAL",
"promptKey": "cortex.security-decision",
"promptVersion": "<bundle version>",
"targetReleaseStatus": "EVAL_ONLY",
"rationale": "Open the shadow run of the candidate bundle"
}
The response is the transition receipt: 200 when applied, 422 with the blockers when blocked. A blank registryScope means PLATFORM_GLOBAL.
Shadow runs and canaries
Shadow run
A shadow run tries a candidate bundle on real requests without letting it decide anything. It is read from the registry's SHADOW release whichever selector chooses the decision's bundle, so a host that runs its configured bundle can still try a candidate.
- Sampling — a request is in the sample by a stable hash of its event ID below
contexa.security.tiered.prompt-bundle.shadow.sample-percent(0 to 100;0, the default, runs none). The shadow runner exists only when this property is set. - Daily cap — at most
shadow.daily-call-capruns (default100) per UTC day, one model call each. - Apart from the decision — the event is copied before the decision, analysed after it on its own executor, and sealed as the
SHADOWstage. The copy never reaches the enforcement handler, so nothing is enforced or learned, and it adds nothing to the session context. - Only runnable bundles — a SHADOW release of a version this build does not run is skipped.
Canary
With selector=registry, a CANARY release runs for the requests inside its share; the others run the latest ACTIVE release. The share is the registry row's canary_percent (0 to 100; no value means the whole scope), applied to a stable hash of the requester (the user, else the session, else the event). Optional canary_tenants and canary_resource_families narrow the canary to those tenants and to requests whose resource address starts with, or whose resource ID equals, a listed family.
Rollback
PromptBundleRollbackService switches the executing bundle of a scope at once to a rollback target its manifest declares. The source becomes DEPRECATED, the target ACTIVE, the runtime caches are told, and the execution is recorded with the conditions it was checked against. A refused rollback is recorded with its reasons and changes nothing.
| Refusal | Condition |
|---|---|
SOURCE_NOT_EXECUTING | The source version is not ACTIVE or CANARY in the scope |
TARGET_NOT_IN_BUILD | The target's manifest is not in this build |
TARGET_NOT_DECLARED | The source manifest does not list the target in rollbackTargets |
VOCABULARY_INCOMPATIBLE | The target's manifest does not read the current vocabulary version |
TARGET_NOT_REGISTERED | The target has no registry row in the scope |
POLICY_SET_MISSING | The target's policy set is missing (a host without policy sets records NOT_APPLICABLE) |
POST /contexa/admin/api/prompt-quality/releases/rollback
Content-Type: application/json
{
"registryScope": "PLATFORM_GLOBAL",
"promptKey": "cortex.security-decision",
"fromVersion": "<executing version>",
"toVersion": "<rollback target>",
"reason": "Decision quality dropped after the release",
"emergency": false
}
The result carries an execution key (RBX-...), its status and the checked conditions: EXECUTED, or REFUSED with HTTP 422. An emergency stop ("emergency": true) runs the same way and ends AWAITING_APPROVAL; another administrator then approves it with POST .../rollback/{executionKey}/approve. The operator who ran it cannot approve it.
Prompt inspector
PromptInspector reads the user prompt the model got from a sealed evidence package and runs each check over it, counting what matched, what did not and what could not be checked. It points at problems for a person to review; it never changes a prompt, a decision or a rule. The automatic round runs on the scheduler's thread under one lock across instances, inspecting the oldest uninspected packages up to the batch size; no decision thread runs it.
| Check | Layouts | What it compares |
|---|---|---|
SEAL_INTEGRITY | All | The package hash covers every sealed field, the system and user prompts were captured and match their hashes, a difference from the rendered prompt is recorded, and the request and correlation IDs are there. |
BUNDLE_TRACE | All | The sealed descriptor names a registry release, and its contract version, artifact hash and approval are that release's. Without a release registry every trace is unverifiable. |
DICTIONARY_CONTRACT | v20 | Line feeds, block headers in dictionary order, only entry lines of their block in order, values in the entry's form or missing text, numbered lines up to the cap, and fact IDs in drawn order. |
DECISION_FACT_MATCH | v20 | Every sealed decision fact the dictionary draws has its line, its value letter for letter and the fact ID sealed with it. |
FACT_ACCURACY | v20 | The facts the prompt draws against their sealed source, field by field and not by rendering the prompt again: in the REQUEST block the subject, time, roles, permissions, action, target, size, class, network and device. A line whose source was not sealed is counted as unverifiable. |
UNKNOWN_DECLARED | v20 | Every fact the engine could not establish is on the UNKNOWN line with its kind, and no fact named there is drawn with a value elsewhere. A missing value is drawn the way the dictionary notes it: an entry with a rendered missing text draws it on its own line (unless the render’s own rule names the fact unknown, for example without a session); an entry the dictionary leaves out is not named on the UNKNOWN line unless the caller marked it so (past records only when the sealed search did not retrieve them); any other entry has its line or its name on the UNKNOWN line. |
PAST_RECORD_NOTATION | v20 | The PAST RECORDS state line counts the records drawn, each record line has its twelve fields, and the order is same target first, then the latest first. |
SOURCE_TRUTH | v20 | The facts against the host's own records, given by a host PromptFactSourceVerifier. Without one the check records that no host source is integrated. |
SAME_FACT | v20 | One fact the prompt states twice has one value. |
REASON_MATCH | v20 | Each reason code of the sealed decision cites an ID on a line of the prompt, that line is one the code cites, and the code's condition holds on the sealed context. |
- Layouts — the v20 checks run only on packages drawn as
v20-dictionary-blocks; a v20 package drawn with another bundle's dictionary or render is counted as unverifiable for those checks. - Verdicts —
PASS(every item matched),FAIL(at least one mismatch),INCOMPLETE(no mismatch, some items unverifiable),NOT_APPLICABLE(not a dictionary prompt; only the layout-free checks ran),ERROR(the package could not be inspected). - Findings —
MISMATCHorUNVERIFIABLE; an unverifiable item never counts as passing. - Versions — every result names the inspector version (
prompt-inspector/N) and the dictionary version (v20@and the head of the dictionary digest). A new inspector version inspects the packages again.
Sealed evidence sampling and retention
In an OSS installation (contexa.enterprise.enabled not true) with capture on (contexa.pqa.oss.sealed-evidence.capture-enabled, default true), decisions are sealed into evidence packages: the system and user prompts as sent, the canonical context and the execution metadata. Enterprise keeps sealed packages in its own schema, so the OSS inspector beans are not created while Enterprise is enabled.
- Window after a change — the first
full-capture-after-changedecisions (default1000) after a start, or after the active bundle changes, are all sealed. - Sample — after the window,
sample-percent(default1, 1 to 100) of the requests are sealed, by a stable hash of the correlation ID. - Retention — a package is kept for
retention.period(default 90 days); one whose latest inspection found nothing wrong is removed afterretention.clean-period(default 30 days). A package whose inspection found something keeps the default retention. - Inspection sample —
contexa.prompt-inspection.sample-percent(default100) of the sealed packages are inspected, by a stable hash of the package ID.
Console and API
Every screen and endpoint requires the ADMIN role.
| Method | Path | Purpose |
|---|---|---|
| GET | /contexa/admin/prompt-inspection | Inspection overview screen (also /packages and /packages/{packageId}) |
| GET | /contexa/admin/api/prompt-inspection/overview | Counts of a period (from, to; the last 7 days by default) |
| GET | /contexa/admin/api/prompt-inspection/packages | Inspected packages, filtered by verdict, check code, block and entry |
| GET | /contexa/admin/api/prompt-inspection/packages/{packageId} | One package with its findings |
| POST | /contexa/admin/api/prompt-inspection/packages/{packageId}/reinspect | Inspect one package again |
| POST / GET | /contexa/admin/api/prompt-inspection/reinspections | Ask the automatic round to inspect a capture period again, and read its progress |
| GET | /contexa/admin/api/prompt-quality/releases | Registry releases (registryScope optional) |
| POST | /contexa/admin/api/prompt-quality/releases/transition | A state transition through the release gate |
| POST | /contexa/admin/api/prompt-quality/releases/rollback | A rollback or emergency stop |
| POST | /contexa/admin/api/prompt-quality/releases/rollback/{executionKey}/approve | The second approval of an emergency stop |
Configuration
| Property | Type | Default | Description |
|---|---|---|---|
contexa.security.tiered.prompt-bundle.active | String | (blank) | The bundle version the configuration selector runs; blank runs the build default. |
contexa.security.tiered.prompt-bundle.selector | Enum | CONFIGURATION | CONFIGURATION or REGISTRY. |
contexa.security.tiered.prompt-bundle.shadow.sample-percent | int | 0 | Share of requests a shadow release runs on, 0 to 100. |
contexa.security.tiered.prompt-bundle.shadow.daily-call-cap | int | 100 | Shadow runs allowed per UTC day. |
contexa.prompt.governance.enabled | boolean | true | Registry, release gate, rollback and release API on or off together. |
contexa.prompt-inspection.enabled | boolean | true | The inspector, its round, re-inspection and console (OSS only). |
contexa.prompt-inspection.interval | Duration | PT30S | Delay between automatic rounds. |
contexa.prompt-inspection.initial-delay | Duration | PT30S | Delay before the first round. |
contexa.prompt-inspection.batch-size | int | 50 | Packages per round (at least 1). |
contexa.prompt-inspection.sample-percent | int | 100 | Share of sealed packages inspected, 1 to 100. |
contexa.pqa.oss.sealed-evidence.capture-enabled | boolean | true | Seal decisions into evidence packages (OSS only). |
contexa.pqa.oss.sealed-evidence.sample-percent | int | 1 | Share of requests sealed after the change window, 1 to 100. |
contexa.pqa.oss.sealed-evidence.full-capture-after-change | int | 1000 | Decisions sealed in full after a start or a bundle change. |
contexa.pqa.oss.sealed-evidence.retention.enabled | boolean | true | Run the retention jobs. |
contexa.pqa.oss.sealed-evidence.retention.period | Duration | 90d | How long a sealed package is kept. |
contexa.pqa.oss.sealed-evidence.retention.clean-period | Duration | 30d | How long a package whose latest inspection found nothing wrong is kept. |
contexa:
security:
tiered:
prompt-bundle:
selector: registry
shadow:
sample-percent: 5
daily-call-cap: 100
prompt-inspection:
batch-size: 50
sample-percent: 100
pqa:
oss:
sealed-evidence:
sample-percent: 1
full-capture-after-change: 1000
The minimum action rule and prompt budget settings are listed in AI Engine Configuration.