Skip to content

candor-java v0.27.0

Choose a tag to compare

@tombaldwin tombaldwin released this 09 Aug 19:52
· 157 commits to main since this release

[0.27.0] — 2026-08-07

  • …and the guard now enumerates a dep directory exactly as the LOADER does. The first repair
    registered the directory's files with a FLAT read beside a RECURSIVE loader walk, so a report one
    level down stayed unguarded — and for --deps, which writes one subdirectory per name@version, the
    nested layout is the ORDINARY one. A guard that enumerates differently from the loader guards a
    different set of files. One enumeration now serves both.

  • ⚠ A --gate-json sink INSIDE a deps DIRECTORY destroyed the operator's dep report. deps
    accepts a directory — --workspace writes .candor/deps/ and hands that back, so it is the common
    spelling — and the loader walks it and reads each report inside. The §3.3.1 sink-over-input guard
    registered only the DIRECTORY, which never equals a file within it, so --gate-json <depdir>/lib.json
    was unguarded: arming destroyed the report, the run chained the wreckage and exited 0 with ok: true
    written over the input. All four engines. The FILE spelling of this channel had been guarded for a
    release; the directory spelling had not, and no row posed it. Now pinned by conformance PART 36 (b14),
    which asserts both the refusal AND that nothing was written.

  • The gate verb arms its --gate-json - stream sink, and one sink is written once. Arming was
    wired into the SCAN pre-pass only, so on the supply-chain surface an exit-2 during argument parsing
    left stdout EMPTY while the same verb refusing later streamed the document — one operator mistake,
    two answers, decided by how early it was caught. Separately, --json --gate-json - names ONE artifact
    twice and both were written, putting two concatenated JSON objects on a stream SPEC §3.1 requires to
    carry the fail-closed document "exactly once, as the streams only content". Both fixed; conformance
    PART 36 gained rows (b4)-(b6), which is why they were found at all.

  • ⟨0.27⟩ The three verdict-document cells (SPEC §3.1/§4, conformance PART 36). (1) The composed
    document no longer carries refused/reason beside violations — the refusal document's
    discriminator must not ride a verdict; the disclosure is unevaluated (this engine already listed
    every rule; an unreadable policy now gets one whole-file entry instead of an empty list). (2) The
    stream sink: --gate-json - gains a shutdown-hook analog of arming, so ANY exit-2 cause (an unknown
    flag, a broken config, a crash) leaves the fail-closed refusal as stdout's only content instead of an
    empty stream. (3) zeroMatch: the §4 zero-match list now rides the verdict document on BOTH routes,
    code-point sorted (Query.BY_CODE_POINT) and deduplicated; it was stderr-only.

  • "(?U)\\s+" written as "(?U)\\s+" — one backslash — made the sink guard blind to a TAB. In a
    Java string literal \\s is JEP-368's SPACE escape, so the guard's regex was (?U) +. A
    policy\\tgate.policy config was read fine by the real loader and not recognised by the guard, so
    --gate-json <that policy> destroyed it and the run exited 0 with "ok": true" while the gate was
    live. The deps split had the same escape and covered no dep after the first.

  • The guard now calls the engine's own config loader instead of re-deriving it. That is the fix for
    the class rather than for the three instances: the hand-written pre-pass also computed the config's
    home directory as parent-of-parent unconditionally (the loader only steps out of a trailing .candor/
    segment, so an out-of-tree CANDOR_CONFIG anchored one level too high and the guard protected a path
    the run never reads), and split deps on the path separator alone. A second parser is a second set of
    holes.

  • ⚠ The gate verb never read the config-declared policy at all — rust, ts and swift all do, so a
    checked-in policy key gated in three engines and refused with "a policy is required" here: a 3-vs-1
    split on the family's own "one config, one meaning" claim, on the supply-chain surface. Caught by a
    new conformance row whose control — assert the config policy GATES before asserting anything about
    the sink — is what found it.

  • trimUnicode named U+00A0 and missed the rest of \\p{Zs}. policy gate.policy\\u202F refused at
    exit 2 here while four engines trimmed it and ran; and a LEADING no-break space left an empty first
    token, so the key became '' and the whole line vanished as "unknown config key" — a policy line
    silently dropped and a MISMATCHED engine pin silently passed. Now the whole space-separator category.

  • The scan target may appear anywhere in argv, matching the other three engines.

  • ⚠ The gate verb got none of the ⟨0.27⟩ guard, so the reference engine still reproduced §3.3.1's own
    worked example.
    gate --report R --policy P --gate-json P exited 0 with "ok": true after
    overwriting P (control, separate sink: exit 1) — the supply-chain surface, the one a consumer points
    at a report someone else produced. The sweep touched Candor.java and skipped Query.java. It also
    still armed mid-flag-loop, so an unknown flag BEFORE --gate-json left the previous run's green while
    the same mistake spelled the other way wrote a refusal.

  • ⚠ The collision guard keyed on the FLAG, so a policy from .candor/config was invisible to it
    the checked-in form, i.e. the one CI actually uses. --gate-json <that policy> destroyed it and exited
    0 with "ok": true, in all four engines. Config's own ⟨0.24⟩ note forty lines away says exactly this:
    "a policy does not change what it is according to how the operator handed it over." The guard now
    enumerates every channel — flag, env, the config's policy/baseline/deps, CANDOR_DEPS, and the
    config file itself — reading the config leniently so it cannot pre-empt the real load's refusal.

  • --gate-json before the target left a stale green. rejectUnknownFlag(args[0], …) ran before the
    pre-pass and java's grammar forced the target into args[0], so candor --gate-json G <target> exited
    2 as an "unknown flag" with yesterday's verdict still at G. The target may now appear anywhere in
    argv, matching rust, ts and swift — one contract, four grammars is what this family exists not to be.

  • The (?U) whitespace fix stopped one line short and became a FALSE REFUSAL. engine<NBSP>java<NBSP>v0.27.0
    — a correct qualified pin — still reached addEnginePin as one token and failed MALFORMED, so this
    engine exited 2 where the other four exited 0; the same for a trailing NBSP on any value, since
    String.strip() uses Character.isWhitespace, which excludes U+00A0 by definition. Fixing a fail-open
    by breaking a working config is the mirror defect, not a stricter reading. deps values stay
    ASCII-split: those are PATHS, and a path may legitimately contain a non-breaking space.