candor-agents v0.27.0
[0.27.0] — 2026-08-07
-
A config-driven exit no longer leaves a stale GREEN verdict at the file sink. The collision
pre-pass loads.candor/configto learn what the sink must not overwrite, and re-raisedSystemExit
— so an unreadable config exited THERE, beforearm_gate_json, leaving a previous run'sok: true
intact on disk. SPEC §3.3 names that outcome: "a refusal that writes nothing leaves the previous run's
green document on disk." The pre-pass now loads leniently and the arming happens; the real load
refuses a moment later with the sink armed. Nothing is lost, because a config nobody can read declares
no inputs anyone can name. Same defect, same fix, as candor-ts. -
⟨0.27⟩ The stream sink and
zeroMatch(SPEC §3.1/§4, conformance PART 36). (1)--gate-json -
now carries the fail-closed refusal document on the exit-2 causes that fired before the gate tail —
a usage error (unknown flag, valueless flag, missing target) and an unreadable policy — instead of
leaving stdout empty; a file sink gets the specific reason in place of the armed placeholder on the
unreadable-policy cause. (2)zeroMatch: the §4 zero-match list now rides the verdict document
(code-point sorted, deduplicated, omitted when empty); it was stderr-only in all five engines. -
⚠ ⟨0.24⟩ A POLICY THIS ENGINE COULD NOT HONOUR WAS SILENTLY REWRITTEN INTO A WEAKER ONE.
deny Frobnicatewas dropped with a stderr note and the run exited 0 printingpolicy ✓— the
operator reads an armed gate that does not exist, which is the fail-open SPEC §6.2 ⟨0.24⟩ closed in
the four code engines by refusing (exit 2, the unreadable-policy posture). Found while measuring the
PART 36 cells. The FATAL set now matches theirs on every token it shares, which is not the same as "token for
token" — an earlier draft of this sentence said that and it was false in two places, both deliberate
and both worth stating rather than quietly matching:-
deny Net[<class>]is HONOURED (scoped, exit 0) by candor-classify for a class it recognises and
FATAL only for one it does not; here EVERY bracketed form is WIDENED — the filter dropped, the rule
enforced unscoped — with a stderr line saying so. So the divergence is wider than a fatal-set
mismatch: a filter candor-classify would apply, this engine discards.(This sentence has now been wrong twice. It first claimed parity "token for token"; the correction
then said candor-classify treats the whole bracketed form as fatal, which is true only for an
unrecognised class — measured:deny Net[known-telemetry]anddeny Net[*]exit 0 there,
deny Net[telemetry]exits 2. Writing a comparison to another engine without running that engine
is what produced both.) This engine emits nonetClass, so honouring the
filter would match an absent field and PASS — fail-open — and dropping the whole rule fails open
too. §3.1's policy-side rule is that a dropped token leaves a WIDER rule standing, so widening is
the third road and the only closed one. A bracketed allow is refused outright rather than
widened, because widening an allow is the direction that hides a violation. -
allow Llmis honoured by candor-classify and refused here:ALLOW_EFFECTSis the four literal
surfaces (Net,Exec,Fs,Db) and Llm is not among them.
Otherwise the set is theirs (candor-classify
not_honoured!(true, …)):
adenywhose effect list ends up EMPTY, anallownaming an effect outside the four literal
surfaces, and an unrecognised reason-class/alias insideUnknown[…]— that last one is the dangerous
direction, sincedeny Unknown[dispatch,nativ]ran asUnknown[dispatch]and let everynativehole
through green. Everything else stays reported-and-survivable (allowwith no values, a malformed
forbid, an unknown rule kind), because the rest of the policy still means what it says. The refusal
reaches BOTH sinks: the file placeholder is replaced with the specific reason, and--gate-json -
carries the refusal document as stdout's only content. There is no composed case to weigh — this
engine has no AS-EFF-005 baseline producer, so every violation it can establish comes from the policy
being refused, and a bad token establishes nothing from the policy itself (§3.1); the site that would
have to consult one is marked. -
-
⚠ This engine declared
spec 0.27and implemented none of the gate-sink layer that release adds.
Measured:--policy P --gate-json PDESTROYEDP, so the very next run of the same command exited 0
on a fleet that violates — the gate silently gone, which is the machine-readable false all-clear the
⟨0.27⟩ rung exists to close. And an unknown flag beside--gate-json Gexited 2 leaving the PREVIOUS
run's green atG. Four engines gained the guard in this release; this one declared the same contract
without it, and conformance PARTs 32/34 ran four engines, so nothing caught it.
Now: the sink is armed fail-closed at the instant it is known — beforeparse_args, whose own exits
would otherwise leave a stale green — and a sink naming an INPUT is refused with nothing written. Every
channel:--policy,CANDOR_POLICY,CANDOR_CONFIG, the config's ownpolicykey (the checked-in
form, i.e. the one CI has), and any.candor/configby shape. Sameness resolves artifacts, not strings. -
⚠ §4 zero-match: a rule whose scope bound no unit was scored as SATISFIED.
deny Exec orchestratr
— one character — turned a failing gate green, silently. Now disclosed on stderr with the verdict and
exit code untouched, because a zero-match rule is legitimate when one policy is shared across fleets. -
A version is ASCII digits, and
str.isdigit()is not.engine ٣.٣(Arabic-Indic) andengine ².0
NORMALISED as versions, so they read as a MISMATCH rather than MALFORMED — and that difference decides
whether the "an unreadable unqualified line is not hidden by a qualified pin" rule fires. Beside a good
qualified pin the junk line was handed over silently and the run passed at exit 0 while three
engines exited 2. Alone, every engine already refused; only the paired shape shows it. candor-swift had
the same defect viaCharacter.isNumber; both fixed, five-way now, pinned by conformance PART 33. -
An unreadable unqualified pin was hidden behind a qualified one.
engine garbagebeside a good
qualified line passed silently here while candor-java exited 2 — unreadability is a property of the