Chio/Docs
LOGIN · JOIN

Behavioral gates

Find the gate that exercises a changed security contract, and learn whether its failure blocks a merge.

Concept chio@5f63e6e

On this page

Eleven behavioral gates guard Chio's security contracts. Each is a script that fails when its contract breaks, and most pair with a self-test that fails when the gate stops noticing. Four gates block every merge. The rest run after a merge, in a trusted Linux container, or by an operator.

Choose a gate by the contract that changed and the observation that would show it broke. A source inventory, a pure decision, a durable store, and a physical confinement boundary each need a different check, and the gate inventory below names which gate reaches which.

Gate reachChoose a run.
committed inventorypull requestinput built to failexercisesexit 0rchangeg′self-testggateccontractpull requestrequired jobpush to mainafter mergetrusted Linuxcontaineroperatorby handblocks merginginventorybuilt to failexercisesexit 0rchangeg′self-testggateccontractpull request · required jobpush to main · after mergetrusted Linux · containeroperator · by handblocks merging

Every named test runs and the gate exits zero

The gate runs its committed inventory against the contract it guards. A test that is missing, renamed, or filtered out fails the inventory check, so a quiet run cannot pass as a full one. Threat coverage.

One gate, its self-test, and the places a gate can run. The inventory length is illustrative; each gate's own self-test and trigger are in the gate inventory.

A gate exercises one contract, and its self-test exercises the gate

A gate is a script under scripts/ that inspects source, runs a targeted test suite, or drives a fixture, then exits non-zero when it finds a violation. Its self-test is a second script that checks the gate's own contract. Most self-tests hand the gate, or a mutated copy of it, an input built to fail and require the refusal.

The runner has failure modes of its own: a filter that selects no tests, a renamed case that drops out of an inventory, a script that discards a failing exit status. Self-tests catch those. A passing self-test says the verifier works; the gate run against the candidate says the behavior holds. Retain each with its own command, source, output, and exit status.

A passing gate covers the layer it reaches

Each kind of check observes a different part of the implementation:

  • Structural gate. A required field or construction site is still present in source.
  • Unit test. A decision holds for the selected inputs.
  • Durable-state exercise. Behavior survives a restart against the durable store.
  • Kernel run. An operating-system restriction is in force on the host.

The tests the gates run assert typed refusals from library functions. The flow gate's engine tests expect FlowDenial::TopSource ("top-labeled data cannot egress") and FlowDenial::DeclassificationReplay ("declassification grant is already consumed"); the deception gate's lifecycle tests expect LifecycleError::IllegalEdge ("decoy lifecycle edge is illegal") and LifecycleError::ReplacementRequired ("an armed replacement is required"). A tool caller sees a signed Deny instead: on the Linux native process host, a top-labeled egress denial arrives with the reason security dispatch outcome requires reconciliation: security native dispatch capture callback failed; authoritative recovery required.

Keep each full gate run's command, trigger, source, stdout, stderr, and exit. A post-push qualification run is reported as post-push, and an interrupted compile is an incomplete attempt even when another self-test succeeded.

Four gates block every merge; the rest report after it

The required Build, lint, test job in ci.yml runs on every push to main and every pull request to main, so its failures block merging. For several gates that job runs only the self-test, and the gate itself runs after a push to main or when an operator starts it.

GateSelf-test runs inGate runs inBlocks merging
temporal
Required job, sha256-checked, bash -p
Required job, sha256-checked, bash -p
Yes
adapter-no-bypass
No workflow
Required job; release qualification after push
Yes
domain-separation
Required job
Required job
Yes
negative-assertions
Required job
Required job
Yes
flow
Required job
After a push to main
Self-test only
deception
Required job
After a push to main
Self-test only
response-recovery
Required job
After a push to main
Self-test only
cage-enforcement
Required job
--release in the trusted container; --portable by an operator
Self-test only
keyring-transparency
None
Trusted container; an operator
No
secret-broker-boundary
None
--release in the trusted container; --portable by an operator
No
authenticated-caller-delivery
None
An operator, for manual qualification
No

The gates that check behavior of the installed product ship with it. An operator runs them on the serving host with chio security selftest, one suite per gate: cage, restart, caller-delivery, deception, temporal, keyring, and broker. Each run is retained and reported as host qualification, and every release records the full gate run behind it, which chio release verify --evidence qualification shows for the release you run.

What each gate asserts, and where its workflow calls it
  • flow. Runs Apalache over the information-flow lattice model in formal/tla/InformationFlowLattice.tla with a reader-direction mutation that must fail, type-checks the flow and security-types crates for wasm32, and checks an exact test inventory with a canonical-flow or flow-sidecar test for each protocol edge and provider adapter it lists. Self-test: check-flow-security.test.sh.
  • deception. Checks decoys, signed watermarks, tripwires, the sealed registry, and canary and honey-tool conformance. The flow and deception gates run after a push to main in the active defense job of the enterprise-hardening.yml workflow that ci.yml calls. Self-test: check-deception-security.test.sh.
  • temporal. Checks temporal rules, event-time correlation, signed security events, and verified event ingress. The Temporal security gate step in the required job checks the sha256 of the runner and of the self-test before running each under bash -p. Self-test: check-temporal-security.test.sh.
  • response-recovery. Checks the response authority daemon, its state machine, fences, freeze, quorum, the crash matrix, and the scheduler's TTL dispatch and lease fencing. The gate runs after a push to main alongside the flow gate. Self-test: check-response-recovery.test.sh.
  • keyring-transparency. Runs RFC 6962 proof vectors plus checkpoint, witness, split-view, and atomic enterprise receipt tests; the witness tests require witness signatures to bind the checkpoint hash and come from a distinct known quorum before a rotation is accepted. It has no self-test. On your host: chio security selftest keyring.
  • secret-broker-boundary. Checks broker capability and proof binding, secret isolation, provisioning, and native kernel custody. It defaults to --release, which runs only on the designated enterprise Linux x86_64 runner and adds the confined broker MCP tests with process death and terminal cage receipts; chio security selftest broker --portable runs the portable inventory on any host.
  • cage-enforcement. In --release mode, draws a fresh 64-character hex random challenge and requires the same value back in the Linux evidence marker. Its self-test, check-cage-enforcement.test.sh, drives that mode against a fake toolchain; chio security selftest cage --portable runs on any host, and on Linux chio security selftest cage runs the random-challenge mode by default. Inside a trusted execution container built from an authorized security source, the Linux enforcement job of enterprise-hardening.yml and enterprise-linux-capture.yml run the keyring gate and the broker and cage gates in --release (see capture authority).
  • authenticated-caller-delivery. Checks the caller delivery exchange. An operator runs chio security selftest caller-delivery as part of host qualification.
  • adapter-no-bypass. Checks the source inventory for a bypass. The Durable consumer boundaries step in the required job runs it through check-consumer-boundaries.sh, and release qualification runs it after a push to main through ci-workspace.sh. Its self-test, check-adapter-no-bypass.test.sh, checks the gate's policy markers and then runs the gate on the current tree; no workflow runs it.
  • domain-separation. Requires every byte-string signing or hashing domain constant under crates/, meaning a &[u8] constant named for a domain or signature, to be declared once and, in production code, to be shaped chio.<area>.<payload>.v<N> with a NUL terminator. Existing exceptions are listed as expiring debt. Self-test: check-domain-separation.test.sh.
  • negative-assertions. Bars a new weak negative assertion in crates/security, chio-kernel, or chio-control-plane; existing sites are pinned one by one by identity. Self-test: check-negative-assertions.test.sh. For both ratchets, renewal through --ratchet can only hold or lower the cap or baseline.

Read more in Engineering standard, which states the rule each gate checks.

The temporal gate keeps a forged event out of every durable table

An attacker keeps a well-shaped detector event and signs it with an untrusted key. The temporal ingress verifier must return an integrity failure before the event reaches any durable correlation or advisory table, while the legitimate neighbor, signed by the configured producer under the same tenant and time expectations, still enters verified ingress. A gate that refused both would show nothing useful.

The temporal suite runs ten test inventories covering rule validation, event-time correlation, producer and receipt provenance, and seven ingress mutations, and it checks the inventory's exact count and digest before any test executes. Run it on the host that ingests detector events:

Terminalbash
chio security selftest temporal --evidence ./temporal-evidence
Output
suite            result
temporal         pass   10 inventories, 40 tests
1 suite passed; evidence in ./temporal-evidence

./temporal-evidence keeps the suite log, its exit status, and a SHA-256 manifest. The suite's database oracle confirms the refused event is absent from security_verified_events, security_correlation_ingress, security_correlation_events, and security_advisory_events, then admits a current valid event and observes one verified row and one ingress row. Those rows say nothing about a network receiver or a physical containment effect.

Seven altered events stop at ingress, and the neighbor still lands

In the pricing project, a forged detector event claims the fixer touched /secrets. The forged case signs the original event body under another producer, so the request reaches signature verification instead of failing in a parser:

Temporal ingress test, excerptrust
#[test]
fn verifier_ingress_rejects_forged_event_without_persistence() {
    assert_ingress_mutation_rejected(
        "forged",
        PortErrorKind::IntegrityFailure,
        |trusted, _receipt_signer, untrusted| {
            let event = current_ingress_event(trusted);
            let signed: SignedSecurityEvent =
                serde_json::from_slice(event.source_evidence.as_bytes())
                    .unwrap_or_else(|error| panic!("decode forged event body: {error}"));
            resign_event(event, signed.body().clone(), untrusted)
        },
    );
}

The full helper builds a fresh SQLite ingress for each case and inspects its tables after the refusal. Forged, unsigned, untrusted, cross-tenant, stale, future-dated, and invalid-receipt events each refuse at ingress, and each case preserves the valid neighbor that still inserts.

Read the three results together. The suite shows the ten inventories ran under their pinned names and refuses missing, renamed, or filtered cases; the typed failure shows the intended boundary refused the altered event; the inserted neighbor shows the boundary still admits useful work. None of them qualifies a Linux cage, a secret, or an external effect, which have their own gates.

Read more in Security events, which covers producer keys and freshness at ingress.

Next: tie a gate to the threat it covers