Skip to content

0.27.1

Choose a tag to compare

@ShkodnikAI ShkodnikAI released this 29 Sep 10:54
5b79996

Security

The audit-driven security release: the v0.27.0 tag predates every fix
below. 0.27.0 is affected — update is mandatory.

  • №510 (gh#794; the consolidated audit 28.09 C-01, High): != in
    rule conditions, flow branch conditions and mutate … rollback_if
    worked as == on the VM (the compiler wildcard arms) — and the rule
    path could not even parse (the grammar deadlock: every comparison
    operator, not just !=, failed to parse in rule conditions); the VM
    rollback kept-table disagreed with the TW for Gt/Ge/Eq/Ne. 0.27.0
    is affected — update is mandatory.
  • №474 (gh#722; the audit 26.09 §3.2, High): the db parity —
    query_row_vm parameters route through the same parameterization as
    the TW lane (convert_params), closing the silent parameter shift.
  • №475 (gh#723; the audit 26.09 §3.1, High): the fs_gate facade —
    one gated FS entry point + the clippy.toml disallowed-methods
    ratchet; the pdf/smtp/config bypass paths are gone.
  • №477 (gh#725; the audit v0.26.1 §3.5): the mcp-serve context
    inversion — the strict-by-default exec context at the entrypoint.
  • №478 (gh#726; the audit v0.26.1 §3.6 + the 27.09 action): the mock
    unification onto the 454 SSOT + the loud env/read_file with the
    explicit-silence *_or naming.
  • Also landed since v0.27.0: №481 (the loud env refusal), №482 (the
    unfreeze-gate CI job), №483 (the media/vision dedup ratchet at zero).

Changed and fixed

  • Naryad №510 (issue #794; the consolidated audit 28.09 C-01, Wave 21
    P0): the != operator is honest end-to-end. THREE defects, one
    class — comparison operators silently degrading, each invisible to
    the gates for its own reason. (1) The compiler wildcard: AstCompareOp
    match arms in mutate rollback_if, the flow branch conditions and
    compile_rule ended with _ => ConditionOp::Eq // Ne and others fall back to Eq — CompareOp has exactly six variants, so the wildcard
    hid exactly Ne: rollback_if: accuracy != 0.9 and flow branches
    cold(x.status != "b") compiled as Eq on the VM backend. All three
    arms are explicit now, and #![deny(clippy::wildcard_enum_match_arm)]
    stands at the top of src/compiler.rs — the lint surfaced 16
    wildcard sites in the file (rules, branches, rollback, BinOp
    fall-through, Statement loops, Expr simplifications, the JIT purity
    disallow-mirror); every one is an explicit variant list, so a new
    enum variant is a compile error until it is consciously classified.
    (2) The grammar deadlock the compiler fix exposed: the
    compare_condition rule (expression ~ compare_op ~ expression)
    could NEVER match — the greedy left expression layers comparisons
    as infix (compare_expr), consumed the whole a != b and left no
    compare_op for the condition. rule If(...) with ANY comparison
    operator (not just !=: >, >=, <, <=, == too) failed to
    PARSE — only contains worked; the consolidated audit's premise
    ("rules accept !=") was lexically true and structurally dead, so
    the compiler wildcard was unreachable dead code on the rule path.
    The parser now accepts an expression-shaped condition and decomposes
    its top-level comparison into Condition::Compare; anything else at
    the top level is a loud parse error. The documented syntax
    (rule If(target.field op value)) now matches reality. (3) The VM's
    mutate kept-table disagreed with the TW interpreter for every
    operator except Lt/Le: Gt/Ge ALWAYS rolled back, Eq was inverted,
    Ne was missing (the always-keep default). The table now mirrors
    interpreter/hooks.rs exactly (kept = NOT(accuracy OP threshold)).
    The diff-fuzzer №465 generated zero rules — the seeded rule block
    (all six operators, deterministic per seed) now rides the program
    output through the flow head, so the class stays visible to CI
    forever. Verified: the №510 contracts C1–C10 green on BOTH backends
    (rule fire/silence, branch taken/not-taken loud consistency,
    rollback kept/rolled-back across !=/>/==); the fuzzer suite
    green with the rule generation; clippy -D warnings clean.

  • Naryad №505 (issue #788; the audit 28.09 §3.5): the naryad number
    uniqueness gate — scripts/ci/naryad_number_check.py + the
    Naryad numbering (blocking) CI job next to the adr-check. Under
    one number different works have already lived (№474, №475, №476,
    №493 — the external audit stumbled on it twice): the gate parses
    the PR title's naryad claim and refuses a number that already
    landed as a DIFFERENT work, listing the occupied works. The
    exception grammar is explicit and pinned by the script's self-test:
    the work-group suffix (№466 group M), the dotted re-issue
    (№475.1), the re-land suffixes (reland/retry/revert/rerun/redo/
    take-N) and the same-issue follow-up (the occupied record pointing
    at the same issue #M). The occupied set comes from the merged
    git history (the EN naryad N and RU Наряд №N subject forms) and
    the closed issue titles via GH_TOKEN (best effort — the history
    alone suffices). The four known collisions are documented in the
    naryad's report so external links stop confusing them. The script
    does not rewrite history and does not rename old naryads.

  • Naryad №495 (issue #780; the audit 28.09 §3.1 High, the P0 core of
    Wave 19): distillation in mlog serve now WORKS on both backends —
    the new server-level DistillHub (src/distill_hub.rs). Before
    this, the per-request fresh program context destroyed every
    recorded example with its request on the TW lane and the VM pool
    reset distill_states on every check-in, so the training threshold
    max(distill_after, 10) was unreachable in serve by construction,
    silently, with no error and no audit event. The hub holds the
    examples, the TEACHING/DISTILLED modes, the trained-model registry,
    the in-flight flags and ONE background training thread per process
    (the №489 posture brought to serve — the VM lane stops training
    synchronously in-request); both backends call it through the
    DistillAccess trait (the DbAccess sample, №474/№484), which also
    collapses the TW/VM distill mirrors — simple_embedding now has
    ONE canonical copy (byte-identical by construction, the №502 mirror
    count moves down). Examples persist in the SQLite table
    distill_samples (the №166 plan shape, with the source
    provenance column) in the server's memory-persist file; weights keep
    persisting through reflex_save. Migration: existing saved weights
    read as before; the example ledger starts accumulating from zero
    (stated honestly here — no backfill exists). Fix-in-shared-point
    corollaries the work surfaced: the TW route body executor now builds
    its context through the SAME fresh_program_context the ticks use
    (the hand-rolled copy had already drifted), and load_program
    registers the learnable table from the program's declarations —
    before it, a VM route body calling a learnable pattern failed loud
    with "learnable index N not found" (main_code never runs on serve);
    the №496 e2e (the audit's headline recommendation) drove both
    findings out. Verified: the e2e green on BOTH serve backends
    (TEACHING accumulation → the audit events distill.training-* →
    DISTILLED answers with the LLM counter frozen → the accumulated
    examples survive a server RESTART); both rollback mutations (hub
    attach off, VM injection off) turn the e2e red; lib 872/0; clippy
    -D warnings clean.

  • Naryad №500 (issue #783; the audit 28.09 §3.2, the hardening core
    of Wave 19): the third-party path-APIs no longer bypass the №475
    filesystem facade. Every pdf load/save site (13 across
    pdf_fill_form, pdf_rotate_page, pdf_delete_pages,
    pdf_extract_images, pdf_merge, pdf_split, pdf_metadata,
    pdf_set_metadata) routes through crate::fs_gate: reads go
    read_bytes(path, purpose) → LopdfDocument::load_mem, writes go
    save_to(&mut Vec) → write_bytes(output_path, purpose) — the
    library's own file open (invisible to any lint) is gone, and the
    audit's three vectors are dead: overwriting app.mlog (the hard
    write deny), writing /home/app/.ssh/authorized_keys (the sandbox
    absolute-path refusal), reading /srv/other-tenant/contract.pdf
    (the read gate). The №500 ratchet extends clippy.toml
    disallowed-methods to the library path-APIs themselves
    (lopdf::Document::load/save, rusqlite::Connection::open,
    image::open, std::fs::copy/rename/remove_dir_all/ create_dir_all) — the explicitly-justified service sites (the
    weights store, the memory/KG/checkpoint journals, the reminder db,
    the doctest harness, vector.rs's post-sandbox open) carry named
    #[allow]s; the pdf output dirs create through the new
    fs_gate::create_dir_all (the full write-gate vocabulary on a
    DIRECTORY path). The write-side canonical re-check mirrors the read
    side's documented one: a symlink named innocently but pointing at
    the application image is refused BY THE POLICY (naming the swap)
    before the OS's O_NOFOLLOW does — pinned by the blocking
    reproductions (7 tests: the three vectors, the .. traversal, the
    symlink swap, the deny-list read, and the legit
    pdf_set_metadata round-trip proving honest pdf scenarios
    unchanged). Verified: grep of direct lopdf path-APIs in pdf.rs = 0;
    mutation checks (raw-write injection → the vectors turn red);
    lib 872/0; clippy -D warnings with the extended list clean.

  • Naryad №506 (issue #789; the audit 28.09 immediate item):
    docs/limitations.md gains the three honest rows the audit asked
    for, before the 0.27.1 tag. (1) Distillation in mlog serve does
    NOT accumulate examples between requests — the per-request fresh
    program context resets the TEACHING state, distill_after is never
    reached across requests (minimal ClassifyIntent example; repair:
    №495). (2) The two pinned ok↔err outcome divergences are
    user-visible as different mlog run vs mlog serve behavior —
    memory_forget compile-refusal in the VM (known_divergences.txt:51)
    and the entity-in-pattern initializer the VM compiles in and TW
    refuses (known_divergences.txt:58); repair: №503. (3) The №455
    sensitive-name deny-list is case-sensitive — register variants
    (SECRET.PEM, .ENV) dodge the name comparison on
    case-insensitive filesystems (macOS/Windows); repair: №501. Each
    row carries its repair naryad link and the loud "marked fixed after
    the merge, never removed silently" contract; a new index section
    "Runtime File-Write Deny-List (№455)" hosts the deny-list row.

  • Naryad №490 (issue #738; the audit 25.09 §4.5 tail): the
    llm/mlogserver/learnable body fields are now ORDER-FREE — the field
    set is fixed, the order is not. The three body rules in
    grammar.pest went from rigid sequences to unordered repetitions of
    their field rules (silent llm_field/mlogserver_field/
    learnable_field choice wrappers keep the inner rules directly in
    the body's child list, so the .find()-based extraction is
    unchanged). A duplicate field is a LOUD parse error naming the
    field and pointing at the duplicate's position — previously the
    parsers' first-wins .find() silently dropped the rest (the
    grammar could not express per-alternative uniqueness; the contract
    moved to guard_unique_field_groups in the AST-building layer).
    The four context_* variants share one logical "context" slot;
    prompt stays multi-cardinality (first-wins is the pinned
    posture); the №757 max_tokens/temperature and the №485
    distill_min_accuracy/distill_margin fields join the same
    free-order set. No required fields exist in the three bodies, so
    the "missing required field" half of the issue's error contract is
    vacuous by construction. Field semantics unchanged — the same
    values land in the same AST fields in any order (pinned by
    permutation-class tests on all three bodies in
    tests/naryad_490_grammar_field_order.rs, 13 tests; the №465/№479
    diff-fuzzer reports no new divergence classes; docs/book/syntax.md
    grammar sections updated).

  • Naryad №493 (issue #772; the №488 test-hygiene residue — 8 findings
    the ignore removal surfaced): every finding the №488 ignore-drop
    exposed is fixed and the nine previously-ignored tests are un-ignored.

    • Anchor 1 (extract_entities silent stub): regex_lite_find took
      the PATTERN string (not the text) and returned an empty Vec
      unconditionally — email/url/phone were never extracted. Replaced
      with three std-only scanners (find_emails, find_urls,
      find_phones) that walk the text manually; 5 unit tests pin the
      behavior.
    • Anchors 2-3 (serve-path respond() lost the leading concat operand
      and direct json_body field access served an empty body):
      parse_status_line unconditionally split on the first space and
      treated the first token as a candidate status — single-token
      bodies (respond("Fosved")) returned the empty string and
      multi-token non-numeric-prefix bodies (respond("Got: " + text + ...)) lost the first token. The fix splits only when the first
      token parses as a u16 in 100..=599; otherwise the whole string
      is the body. The legacy respond("200 OK") form is unchanged.
    • Anchor 4 (recall returns the first hit ≥ threshold, not the best):
      the .find() in recall_tw is replaced by filter + max_by so
      the best qualifying signal wins regardless of iteration order;
      AND the sim computation now checks substring match FIRST
      (value.contains(query) → sim = 1.0) before falling back to
      cosine — the TF-IDF default embedding's process-local vocab makes
      cosine-only matches spurious across interpreter sessions, and the
      substring lane was never reached because cosine was computed
      first. Same sim-order fix in SqliteStore::recall,
      SqliteStore::recall_top_k, and the MemoryStore::recall_top_k
      default.
    • Anchor 5 (forget/recall consistency): the < cutoff comparison
      let forget "X" after 0.days keep entries stamped at the same
      second — cutoff == now and created_at < now was false. The
      <= semantics match the user-facing "after N days" intent.
      Both InMemoryStore::forget and SqliteStore::forget fixed.
    • Anchor 6 (KG recall across restarts returns ''): a program that
      runs only relate "a" to "b" as "r" (no memorize) persists
      edges to SqliteKg in Session 1; Session 2 opened the KG but
      recall_tw never asked it for the query string when both the
      store lane and the typed lane came up empty. The fix adds a third
      fallback: kg.edges_for(query) synthesizes the [GRAPH] body
      when the store and the typed lane both miss.
    • Anchor 7 (parser thread panic on test "p" { ... }):
      unescape_string did &s[1..s.len()-1] to strip outer quotes,
      contradicting its own docstring; parse_test_decl pre-stripped
      with s.trim_matches('"') and panicked for one-char literals.
      The function is now the single source of truth (strips when
      present, guards short inputs, tolerates already-stripped strings)
      and parse_test_decl passes the raw lexeme.
    • Anchor 8 (id collisions when two records land in the same second):
      the mt_<sec>, mt_l1_<sec>, mt_l2_<sec>, cron_<sec> and
      appr_<sec> ids collided on the second-precision timestamp. The
      new chrono_now_id_suffix() (20-digit zero-padded nanos + 6-digit
      per-process atomic counter) is the tiebreaker — lexicographic
      chronological order within the same id prefix is preserved.
  • Naryad №489 (issue #737; the audit 25.09 §3.11 — training happens
    inline): the distill training moved OFF the request path. The
    attempt call now spawns a BACKGROUND trainer and returns
    immediately — the 30-epoch run no longer lives in the serve request
    latency. The registry handle is Arc<Mutex<ReflexRegistry>> (the
    .lock() sites unchanged); the trainer thread ships its verdict
    (switch decision + audit lines) through an interpreter mailbox; the
    distill path DRAINS the mailbox on every call — the verdict lands
    when it lands, and until then the pattern stays TEACHING (the
    issue's "до готовности — TEACHING-поведение"). Status is LOUD:
    distill.training-started (at spawn) and
    distill.training-finished (on drain, with the switch verdict or
    the error) bracket every run. Consistency — THE QUEUE CHOICE
    (documented per the issue's either/or): the examples ledger keeps
    accepting records while a trainer runs (a snapshot is what trains);
    the next cadence cycle retrains on the fuller set. One trainer per
    pattern (the in-flight mark; never a second concurrent trainer).
    The switch no longer re-enters DISTILLED on the triggering call —
    it lands on a later call via the drain. THE VM PATH STAYS INLINE,
    loudly: the VM is per-request (№388), so a detached trainer would
    outlive its registry and the verdict could never land — for the VM
    the inline form is the lifecycle guarantee, the issue's task-3
    honest-refusal option. The synchronous seam
    (try_train_distilled_model) is kept for the unit tests and now
    delegates to the same shared training core the thread uses. Tests:
    the attempt call with a 50 000-example set returns in < 250 ms (the
    sync path would have blocked for the whole run); exactly one
    trainer per pattern; the bounded-wait drain lands the verdict and
    flips the mode; the №456 holdout refusal rides the background path
    (loud rejection, no flip). lib 867/0 (the №456/№485 pins 12/12
    unchanged); clippy -D warnings + fmt clean.

  • Naryad №488 (issue #736; the audit 25.09 §6.2 tail — the test debt
    grows no more): the test-hygiene wave — 36 #[ignore] attributes
    lifted, honestly. The phase23 (v0.8.4–v0.8.7) suite is ACTUALIZED to
    the current surface: every runtime program moved into test blocks
    (top-level statements are a dead syntax), the 6 semantic-arity tests
    lifted as-is (their patterns only needed the now-mandatory
    -> Unit), learn_preference calls fixed to the real 3-arg shape —
    which exposed a REGISTRY/IMPL mismatch (the spec said 2, the impl
    requires 3 — the registry is corrected to 3 and REFERENCE.md
    regenerated). The persist suite's stale "flaky, need temp dir
    isolation" reason lifted (the tempdir isolation was there all along;
    5 tests pass deterministically); the webhook harness reason lifted
    (run_test_server has existed since №455; 1 test passes). The debt
    counters moved ONLY DOWN: ignore 85 → 58, ignore_todo 49 → 22 (the
    baseline re-thresholded in the same PR). The remaining 9 ignores
    carry precise findings with TODO(№493, revise 2026-10-15) dates —
    they are NOT harness debt but real behavioral findings (the
    extract_entities extraction core is a silent no-op stub; the serve
    path loses the leading concat operand and mis-serves direct
    json_body field access; the recall lane returns the first hit, not
    the best match; the forget/recall consistency and the KG recall
    across restarts) — all tracked in the new naryad №493 (gh#772). One
    test was DELETED per the honesty rule: test_extract_entities
    pinned a silent stub as its contract (see №493). Three latent
    runtime defects surfaced and are documented in №493: the parser
    panics on one-character string literals (unescape_string's
    byte-slice), and the cron/mtree ids collide within the same second
    (cron_<sec> / mt_<sec>) — the tests neutralize it with a 1.1 s
    spacing until the runtime fix lands. 20 + 5 + 8 tests green
    across the three suites (×3 local runs for the flake-prone ones).

  • Naryad №487 (issue #735; the audit 26.09 action item 11 — the
    reconstruction, the §3.7 text never delivered): the mock-response
    marker hash is an EXPLICITLY stable algorithm — FNV-1a 64
    (offset basis 0xcbf29ce484222325, prime 0x100000001b3), the
    marker hex being the LOW 32 bits of the digest of the prompt's
    UTF-8 bytes, {:08x}. DefaultHasher never promised cross-release
    stability (std pins its keys within a version, not the algorithm
    across toolchain upgrades) — a Rust upgrade could silently move
    every [mock-llm:<8 hex>] marker in the goldens and logs. The
    formula, the constants and the truncation are documented in the
    source; the new test_mock_response_golden_hex pins the hex for
    three prompts (including the empty prompt = the offset basis) and
    breaks on purpose if the algorithm ever moves. The marker VALUES
    changed exactly once with this merge — the mock-mode goldens were
    updated deliberately, all 21 markers across 9
    examples/*.expected files (m3_classify, p2_full_adapt,
    p11_context_loading_fixed, p12_context_{auto,literal,none},
    reflex_distill_{teaching,switch,fallback}); the full old→new
    mapping is in the naryad report. The marker FORMAT is unchanged.

  • Naryad №486 (issue #734; the audit v0.26.1 §3.9 — the typed stage-0
    signatures were documentation, not a checked contract): the
    declared-type conflict surface, WARN-ONLY, on top of the №474
    stage-1 machinery. Three checks in the same semantic pass, own
    grep-able prefix [n486 types]: R1 — a pattern/tool method declaring
    -> T that returns a KNOWN different type (a direct typed builtin
    call, a literal, a typed variable, or a pattern call under its
    declared return) warns WITH POSITION (the parser now carries real
    spans for Assign/Return statements); R2 — declared parameter
    types (x: String) seed the inference environment, so the existing
    №474 assign rule flags x = <differently-typed builtin> with the
    origin naming the DECLARATION; R3 — pattern and learnable-pattern
    calls type their let from the DECLARED return type, so a later
    builtin reassignment of a different known type conflicts. Honesty
    pinned by tests: an unparseable declared spelling (-> Message)
    never warns; a declared Fluid never warns (it accepts anything by
    design); labels are ERASED before comparing (String<private>
    compares as String); Unknown never warns (the №474 pin). The
    language stays dynamically typed — every conflicting program still
    compiles and runs (the warn-only proof). The firing count across
    examples/ ships as an INFORMATIONAL CI artifact (the
    test-integration job prints it; NOT a ratchet — the issue says so):
    the corpus of 242 checkable programs carries 0 today. 10 tests in
    tests/naryad_486_declared_types.rs (3 catches + warn-only +
    honesty + the corpus discipline); the №474 pins stay green
    unchanged.

  • Naryad №485 (issue #733; the audit v0.26.1 §3.8 — the distill gate
    passes a degenerate model on skewed data): the majority-baseline
    gate + the stratified split + the NaN-safe refusal form. The switch
    now requires `holdout_accuracy ≥ max(min_accuracy, majority_baseline

    • margin) (distill_margin: , default 0.05) — the audit's 90/10 skew case (a model that always answers the most-frequent class scores 0.90 and cleared min_accuracy=0.85while learning nothing) is refused by construction: its accuracy equals the baseline, and baseline < baseline + margin. THE CHOICE (fixed per the issue's either/or): baseline + margin over balanced accuracy — one deterministic number, no confusion matrix. A single-class dataset carries no confusion risk — the raw gate applies unchanged (the №456 posture preserved). The holdout split is STRATIFIED by class (per-class seeded 80/20, every present class keeps train coverage) — the holdout mirrors the skew and the baseline reads true. The refusal form is the negated positive!(holdout_acc >= t): a NaN accuracy is rejected automatically. The distill.rejected audit event carries the baseline and the margin. Pins: the degenerate 90/10 reproduction (TW + VM mirror — the raw gate would PASS it), the separable two-class positive control, and the grammar suite (distill_margin:optional, rides the AST through bytecode;#[serde(default)]` keeps the old .mbc
      deserializable). Honest side effect: the reflex_train_predict
      golden loss changed (0.079445... to 0.078831... — the stratified
      partition trains on a different 80%); accuracy, prediction and
      the gate outcome are unchanged.
  • Naryad №484 (issue #732; the audit v0.26.1 §3.3, Medium — the
    literal metric is a Goodhart surface): the SECOND dedup metric + the
    first unified pair. The new blocking CI job ops-pairs counts the
    *_tw/*_vm LOGIC pairs in the shared *_ops.rs modules
    (scripts/ci/ops_pair_counter.py, the checked-in baseline
    ops_pair_baseline.txt, the threshold moves ONLY down) — a new pair
    fails CI, a removed one moves the threshold to the new fact. The
    rename VmDbAccess → DbAccess (the vm_ method prefixes drop):
    the state trait is now backend-neutral, and the interpreter lane
    rides it too — the TwDbAccess adapter locks the TW's
    Mutex<Option<Connection>> for the statement's duration. The FIRST
    pair unified: query_scalar — the ONE suffix-free function over
    impl DbAccess on both backends (the former bodies were
    byte-identical from the statement execution onward; the unified fn
    keeps the richer error texts on each axis — the SQL-arg type detail
    from the TW side, the №758 loud not-open reason from the VM side).
    Pins: tests/naryad_484_db_unification.rs — the static
    Vm: DbAccess witness, the TW adapter shapes (Float / Unit / the
    typed №381 bind), the unified error texts, and the end-to-end
    TW↔VM parity of the same query_scalar program.

  • Naryad №483 (issue #731; the audit-recommendations 26.09 P1 "the
    dedup to the fact" + the open №466 tail): the media/vision group of
    the TW/VM duplicate-name transfer — the FINAL group of the №466
    лекало. The twenty duplicated names' spelling moved to the shared
    live module src/media_ops.rs (the name constants + the handles()
    membership hook); both backends now compare against the constants —
    the per-backend dispatch bodies stay (the №480 boundary: a NAME
    transfer, not a logic unification; the pair metric is №484). The
    №462 counter's fact is ZERO duplicated names: the ratchet lands at
    60 → 56 → 49 → 42 → 35 → 20 → 0, the CI threshold = the fact (a new
    duplicate name fails CI at once). The transfer is feature-independent
    (pure constants — the names resolve on every build; the
    no-default-features build is unaffected). Pins:
    tests/naryad_483_media_dedup.rs — the handles() set is exactly the
    transferred twenty (the registry's media/vision surface is 23:
    media_manifest_read, vision_fetch_weights, vision_understand
    were never TW∩VM duplicates), every owned name is registered, the
    constants spell the registry names. Drive-by docs honesty: the №482
    CHANGELOG entry landed with stray diff-style + line markers —
    stripped and re-indented (the rendered text unchanged).

  • Naryad №482 (issue #730; the audit-recommendations 26.09 P1 —
    "unfreeze criteria machine-checkable"): the unfreeze-gate CI job.
    The four ADR-0177 §4 criteria now land in ONE machine-readable
    summary (unfreeze_summary.md, the build artifact unfreeze-summary

    • the GitHub step summary) with an explicit verdict per criterion:
      (4.1) Types — the typed-signature share vs the checked-in floor
      (№467 gate); (4.2) Dedup — the TW/VM duplicate-name count vs the
      №462 threshold; (4.3) Debt — the №468 ignore/dead_code counters;
      (4.4) Memory — the office E2E dogfood (office#373, FO-056): the
      in-repo twin (tests/naryad_429_memory_office_path) runs LIVE in
      the job, and the office-side facts ride the checked-in evidence
      record scripts/ci/office_e2e_baseline.txt (the office repo is
      private — no cross-repo token; the record is fail-closed and
      refreshed by the office-side naryads). The job is BLOCKING: a RED
      anywhere fails it, so the release gate (ADR-0177 §6: 0.27.0 is not
      published while any §4 criterion is red) reads evidence, not
      intentions. The summary carries the release-blocking label and
      SURFACES findings without changing any threshold — including the
      observed dual-floor record in the type baseline (the raised 998 bp
      recorded while the gate enforces the landing 980 bp; the convergence
      belongs to the №467 raise procedure). The summary decides nothing —
      the lift belongs to the OWNER ONLY (ADR-0177 §4).
  • Naryad №481 (issue #729; the audit 25.09 §3.9 tail — "open, io.rs:183
    unchanged"): loud env/read_file config errors + the *_or
    explicit-silence naming. env() of a MISSING variable now refuses
    LOUDLY ([ENV_NOT_FOUND], naming the variable and pointing at
    env_or) — the old soft empty string masked misconfiguration. The
    new env_or(name, default) builtin carries the silent-default
    semantics IN THE NAME (the audit naming rule): the fallback firing is
    announced on the audit stderr (the variable NAME, never the value)
    and the SAME №259 serve-route gate applies — explicit silence never
    bypasses the env policy. read_file keeps the documented №254
    missing-file soft contract ("" — now pinned), but a file that EXISTS
    and cannot be opened/read is a config/environment error: refused
    LOUDLY ([IO_ERROR] with the OS reason) instead of a silent "".
    Migration honesty: programs that relied on the empty-string default
    switch to env_or(name, ""); the golden examples were verified —
    every executed env() either reads a set variable or sits in a dead
    branch, and the executable doc block that DID read a missing variable
    (syntax.md Entity (simple)) was migrated to env_or — the
    REFERENCE env/read_file rows (curated + generated index) now carry
    the split contract. New pins: tests/naryad_481_loud_env.rs (loud
    env both backends + VM parity, env_or silence + VM parity, the
    fallback announcement through the real binary with the VALUE-never-
    logged assertion, the serve-gate precedence, loud unreadable
    read_file, the soft missing-file contract);
    tests/naryad_259_env_gate.rs re-pinned — a missing (even
    allowlisted) name inside serve is now the loud 500.

  • Naryad №477 (issue #725; the audit v0.26.1 §3.5 finding — mlog mcp-serve with the http/sse transports is the same network server
    executing user code as serve, but ran on the pre-№457 model): the
    mcp-serve inversion. cmd_mcp_serve sets the process mode to Serve
    at the entrypoint (the №457 cmd_serve precedent) with the loud
    strict-by-default banner — every UNMARKED thread is ServeRoute
    (env() №259-gated, exec() №253-gated), so background tasks,
    resource-handler calls and future MCP entry points fail LOUD instead
    of silently keeping process rights. The tool-call surface does NOT
    degrade: tools/call keeps its explicit ServeRouteExecGuard
    (№401, execute_tool_method) — identical behavior on every transport
    (stdio included), pinned by the tool-call parity test under the
    inverted default (the №259 refusal without the serve flag, the same
    read with it). The tool-registration phase keeps the process context
    (TopLevelRegistrationGuard inside McpServer::new, the №457
    precedent) — tools/list with the compiled №316 policy meta is
    unregressed. Pinned in tests/naryad_477_mcp_serve_inversion.rs
    (the №457 test pattern: an unmarked background thread → ServeRoute
    with the loud env/exec refusals; the registration-zone machinery;
    the tool-call parity; the tools/list surface).

  • Naryad №478 (issue #726; the audit v0.26.1 §3.6 finding + the 27.09
    fact-check enrichment: the retired env name was re-read in 7 places,
    not 1): the mock-unification onto the №454 SSOT. All 7
    METALOGOS_LLM_MOCK re-reads (call_llm, call_llm_schema, the office
    human surface, voice/ocr/vision/video understanding) now route through
    crate::llm::mock_llm_requested() — the deterministic mock answers
    ONLY when METALOGOS_MOCK_LLM=1|true is set explicitly. For the media
    surfaces (stt/omni/ocr/vision/video/office-human) the default changes
    from "silent stub" to "loud refusal without weights" (the №294
    real-path stays parked; the refusal names the backend, the weights id
    and the PARKED boundary — never a silent mock-substitution, the exact
    behavior class №454 closed). Golden examples opt in per example via
    their .env sidecars (the №454 posture, ADR-0169 §3.4) — five
    LLM-dependent examples carry the explicit METALOGOS_MOCK_LLM=1
    now; the module docstrings and the REFERENCE/limitations rows are
    synced. New BLOCKING CI job mock-env-ssot: the retired env name
    must never reappear in src/ (grep gate). The mock-dependent suites
    (№334/№336/№407, the №385 fault-seam codes, the №381 Stage-4 serve
    parity) were re-pinned to the explicit-mock contract — the fault
    seam is a MOCK fault and the corpus llm_classify route rides the
    deterministic mock, so both pin the opt-in themselves.

  • Naryad №479 (issue #727; the audit v0.26.1 §3.4 finding — the
    TW↔VM diff fuzzer is right in approach but narrow, with an
    unreviewable corpus): the fuzzer v2. The class signature is now the
    STABLE ERROR CODES of both sides plus the AST node kind each error
    was born at (errorclass|tw=err:[TYPE_MISMATCH]@fn_call|vm=...) —
    stable codes were assigned FIRST at the origin sites of BOTH backends
    (UNDEFINED_VARIABLE on the TW ident read and the VM rule path,
    UNDEFINED_FUNCTION on the VM compile refusal, TYPE_MISMATCH on
    the heterogeneous concat/binary-op refusals, the TW each-over-
    non-List refusal, the len()-argument and *_arg builtin refusals;
    ADR-0131 convention, parity-pinned). The corpus is human-readable and
    machine-checked: every signature line carries a `# class: /

    example: / # status:` comment block, and each example is a

    LOAD-BEARING checked-in minimal program that reproduces its class
    every run — when it stops, the run fails demanding the line and the
    example be removed in the fix PR (the "class disappeared = the gap
    closed" ratchet self-enforces). The 8 pre-№479 normalized-prose lines
    collapse into 5 coded classes (the same root causes, readable
    signatures). The generator is STATEFUL: in-memory SQLite
    (db { url: "sqlite::memory:" } + query/db_execute), the
    deterministic LLM mock (call_llm), and try smtp_send (the
    deterministic config-refusal — the honest MockSmtp; no connection is
    ever attempted), on top of the memory group — the duplication zones
    the audit named are exercised by the fuzzer itself, and the №476
    blocked-domain rule applies at full strength to everything it finds.
    The report carries the RAW side texts and the program's AST
    node-kind view; err:uncoded(...) sides fail loudly until the origin
    site gets its stable code.

  • Naryad №758 (issue #758; the owner's VM-office bug report,
    FOSVED-office-v2 differential probe, re-confirmed on 0.27.0): the VM
    lane resolves db { url: env("NAME") } now — the compiler classifies
    the URL source and carries the NAME (Program.db_url_env,
    #[serde(default)] so old .mbc artifacts load), the URL itself
    resolves at the FIRST db access (the interpreter's runtime semantics;
    a compile-time constant fold was rejected — it would bake
    credentialed URLs into the bytecode). The resolution goes through the
    same env() gate as a program-level call (№259 SSOT: the serve-route
    policy + METALOGOS_ENV_ALLOWLIST). The two SILENT no-ops become
    LOUD: an env denial/unset and an unsupported (non-sqlite) scheme both
    store the reason (Vm.db_open_error) that every VM db access site
    surfaces instead of the "no database connection" riddle — the
    postgres class now gets an honest error naming
    METALOGOS_SERVE_BACKEND=interpreter as the escape hatch. Exotic URL
    expressions (neither a literal nor env("NAME")) are a loud compile
    error instead of the old silent broken state. The plain no-db case
    keeps the legacy text. (The office differential probe — interpreter
    200 / vm 500 on the same file — is closed: the vm lane connects at
    the first db access like the interpreter does at boot.)

  • Naryad №757 (issue #757; the FOSVED-office-v2 report autopsy,
    2026-09-27): call_llm no longer hard-codes the generation ceiling —
    the six "max_tokens": 1024 literals across the legacy RealLlm
    (anthropic/openai) and the SmartRouter (call + both stream opens)
    read the shared configuration: llm { max_tokens: N, temperature: X }
    (validated loudly at parse — N ≥ 1, 0.0 ≤ X ≤ 2.0; either field
    order accepted; call_claude reads the same surface) → the
    METALOGOS_LLM_MAX_TOKENS env fallback → the new default 4096.
    BEHAVIOR CHANGE: the default ceiling rises 1024 → 4096 (1024 tokens
    ≈ 600–800 words of Russian text — every structured office report was
    being cut mid-sentence). The truncation is now VISIBLE, not a silent
    success: finish_reason/stop_reason/done_reason is read on every
    provider branch INCLUDING the SSE streams (OpenAI
    choices[0].finish_reason, Anthropic message_delta.delta.stop_reason,
    Ollama done_reason); a length truncation prints a stderr warning
    naming the fix, lands in the JSONL trace as the finish_reason field
    (№276 honesty: absent stays absent), and is checkable from mlog via
    the new llm_last_finish_reason() builtin (registry 500→501,
    append-only, typed String — the №467 share floor rises 980→998 bp).
    The per-call override is a documented non-goal (follow-up); the mock
    and error paths never invent a reason ("" = none observed yet).

  • Naryad №475 (issue #723; the audit 26.09 §3.1 High): the filesystem
    FACADE — src/fs_gate.rs is the one gated entry point for every
    program-influenced file operation (open_read/open_write/removal/
    directory listing), each applying the sandbox, the №455 deny-list on
    BOTH reads and writes, and the serve data-dir containment on both
    directions. The HARD write-deny (task 5): *.mlog, .env*,
    metalogos.toml, .git/** refuse unconditionally — the allowlist
    crane does not apply to the application image. The deny-list gained
    the SQLite sidecars (*.db-wal/-journal/-shm) and the credential
    classes (*.pem, *.key, id_rsa*, id_ed25519*, .netrc,
    .npmrc, credentials*). Converted to the facade: the io.rs
    write/append/delete/list builtins, pdf.rs (all reads/writes; the
    external wkhtmltopdf output is gated before the spawn), smtp
    attachments, send_document, http_post_multipart file fields,
    http_download, config_load, consent/ledger exports, media_save,
    memory_export, tts_generate, vision exports, video mux. The ratchet
    (task 2): clippy.toml disallows the raw std::fs methods — any new
    bypass is a CI error (the justified service modules carry an explicit
    #[allow] with a reason: the weights store, the journal, the
    compile-time source loaders, the docs/test harness).

  • Naryad №474 (issue #722; the audit 26.09 §3.2 High): the db_execute
    TW/VM parity — ONE contract on both backends, the affected-row count
    as a String (the VM lane raised from Unit to the TW form); the
    query_row VM params now bind TYPED through the №381 convert_params
    SSOT — the old stringify lane silently dropped unsupported values
    (a filter_map with _ => None), shifting the positional $N
    placeholders (the audit's worst case: a query with a different
    semantics EXECUTED). An unsupported param value is now a loud error
    naming the 1-based parameter position on both backends; the
    divergence-pinning unit test is rewritten as a correctness regression
    test (typed binding, no positional shift, loud positioned refusal).