The fail-closed posture
Novarch blocks a write it cannot get a judgment on. That is the default, and a deployment that has not changed it never releases one.
The posture is the deployment’s answer to what should happen instead. A customer admin sets it in ADMIN → SETTINGS. No SDK option reaches it and no agent chooses its own. Where it has been set to release, your agent sees two answers it would otherwise never see, and a write that used to raise now runs.
The three values
| Posture | What the gate does | Where it can be set |
|---|---|---|
fail_closed | Refuses the write. Your code gets a NovarchKillError with a [fail_closed_*] prefix. The default. | Deployment-wide, or per action type |
fail_open | Releases the write with no judgment behind it. | Deployment-wide, or per action type |
fail_open_declared | Releases the write, naming a compensating control in the admin’s own words. | Per action type only |
There are two levels: a deployment-wide default, and an override for a single action type. An action type with no override is governed by the default.
fail_open_declared is per action type because the control it names has to sit
beside the thing it compensates for. A deployment-wide one would be a single
sentence covering every action type the deployment gates.
What your code sees
Both answers run the write and log a warning. Neither raises, and neither is a word to branch on: the SDK classifies it and your gated function executes.
unjudged, when the judge could not answer
The judge errored or timed out and the posture released the action. The submit
response carries unjudged where a verdict would be, along with the id of the
authorization the gate recorded, and your write runs.
Under fail_closed the same failure is a NovarchKillError carrying
[fail_closed_judge_error].
released, when a pause ran out
The engine paused the action, nobody decided it, and your process reached the end of its wait. The SDK reports the timeout, the gate releases the hold, and your write runs.
Under fail_closed the same report is a NovarchKillError carrying
[fail_closed_operator_timeout].
Where the warning goes
Python routes both lines through logging.getLogger("novarch.gate") at
WARNING. TypeScript uses console.warn, because the SDK carries no runtime
dependencies and console is the one output Node, Workers, Deno and Bun all
share.
import logging
# Python: route the gate's warnings wherever your ops alerts already go.logging.getLogger("novarch.gate").addHandler(your_handler)Send those warnings somewhere a person reads. The deployment records every release in its own ledger and pages Slack about it, and neither of those is where the person who owns the agent looks. Without the line, a fail-open night reads as a clean night in the only log your process writes.
Where the posture speaks, and where it does not
A pause is released only while your agent is still waiting for the answer. Four things can reach a paused action nobody decided, and they differ in whether a process is still there:
| How the pause ends | Under fail_closed | Under a releasing posture |
|---|---|---|
| Your process reaches the end of its wait and reports the timeout | NovarchKillError, [fail_closed_operator_timeout] | Released. Your write runs. |
| The deployment’s sweep reaches it a minute after the window closed | denied, [fail_closed_operator_timeout] | The pause expires undecided. Nothing is authorized and no write runs. The action stays answerable and carries an overdue mark in the operator’s queue. |
| The server restarts past the hold’s grace window | denied, [fail_closed_restart] | Nothing is written. The action stays paused, and whichever of the other rows reaches it next settles it. |
| The session completes with the hold still open | denied, [fail_closed_operator_timeout] | Denied. A session that is ending has no process left to run the write. |
Only the first row runs your write, because it is the only one where a process is still there to run it.
A killed session releases nothing under any posture. Once an operator throws the kill switch, every waiting hold inside that session is denied.
A release is not an approval
The record says the gate let the action through with nobody’s decision behind
it. On an unjudged release nothing evaluated the action at all. On a released
pause a judge did rule and did cite a rule, and no person answered before the
window closed.
That is the whole of what the deployment observed. Whether the write then succeeded happens outside Novarch, and the audit packet for a release says so.
Your operators see it too. A released crossing shows up in MONITOR with a NO JUDGMENT chip when the judge never answered, or NO DECISION when a pause ran out, and the UNGATED tile counts both. It is never folded into CLEARED, because nobody cleared it.
Which SDK understands these
Both answers are new in novarch 0.4.0 and @novarch/sdk 0.4.0.
An SDK that does not know a word the gate sends refuses the write and raises. Write your own client against the API and owe it the same rule: a verdict you do not recognise is a block.
Keep the SDK and the server on the same major.minor. A mismatch gets one
explicit 400 on every gated request, surfaced as a fail-closed
NovarchKillError, so the two move together.
Next steps
- Configuration: how long a paused action waits, and how your session declares what it can hold.
- NovarchKillError: every way the gate refuses a write, and the prefixes to branch on.
- Concepts: the flow, grounding, and the kill switch.