Repository navigation
0.27.1
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,flowbranch conditions andmutate … 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_vmparameters 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 + theclippy.tomldisallowed-methods
ratchet; the pdf/smtp/config bypass paths are gone. - №477 (gh#725; the audit v0.26.1 §3.5): the
mcp-servecontext
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 loudenv/read_filewith the
explicit-silence*_ornaming. - 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 inmutate rollback_if, theflowbranch conditions and
compile_ruleended with_ => ConditionOp::Eq // Ne and others fall back to Eq—CompareOphas exactly six variants, so the wildcard
hid exactlyNe:rollback_if: accuracy != 0.9and flow branches
cold(x.status != "b")compiled asEqon the VM backend. All three
arms are explicit now, and#![deny(clippy::wildcard_enum_match_arm)]
stands at the top ofsrc/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_conditionrule (expression ~ compare_op ~ expression)
could NEVER match — the greedy leftexpressionlayers comparisons
as infix (compare_expr), consumed the wholea != band left no
compare_op for the condition.rule If(...)with ANY comparison
operator (not just!=:>,>=,<,<=,==too) failed to
PARSE — onlycontainsworked; 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 intoCondition::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 warningsclean. -
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 sameissue #M). The occupied set comes from the merged
git history (the ENnaryad Nand RUНаряд №Nsubject 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 inmlog servenow WORKS on both backends —
the new server-levelDistillHub(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
resetdistill_stateson 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
DistillAccesstrait (theDbAccesssample, №474/№484), which also
collapses the TW/VM distill mirrors —simple_embeddingnow 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 thesource
provenance column) in the server's memory-persist file; weights keep
persisting throughreflex_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 SAMEfresh_program_contextthe ticks use
(the hand-rolled copy had already drifted), andload_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 eventsdistill.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 warningsclean. -
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 throughcrate::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: overwritingapp.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 extendsclippy.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_metadataround-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 warningswith the extended list clean. -
Naryad №506 (issue #789; the audit 28.09 immediate item):
docs/limitations.mdgains the three honest rows the audit asked
for, before the 0.27.1 tag. (1) Distillation inmlog servedoes
NOT accumulate examples between requests — the per-request fresh
program context resets the TEACHING state,distill_afteris never
reached across requests (minimal ClassifyIntent example; repair:
№495). (2) The two pinned ok↔err outcome divergences are
user-visible as differentmlog runvsmlog servebehavior —
memory_forgetcompile-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 (silentllm_field/mlogserver_field/
learnable_fieldchoice 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 toguard_unique_field_groupsin the AST-building layer).
The fourcontext_*variants share one logical "context" slot;
promptstays multi-cardinality (first-wins is the pinned
posture); the №757max_tokens/temperatureand the №485
distill_min_accuracy/distill_marginfields 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_findtook
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_lineunconditionally 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 legacyrespond("200 OK")form is unchanged. - Anchor 4 (recall returns the first hit ≥ threshold, not the best):
the.find()inrecall_twis replaced byfilter + max_byso
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 inSqliteStore::recall,
SqliteStore::recall_top_k, and theMemoryStore::recall_top_k
default. - Anchor 5 (forget/recall consistency): the
< cutoffcomparison
letforget "X" after 0.dayskeep entries stamped at the same
second —cutoff == nowandcreated_at < nowwas false. The
<=semantics match the user-facing "after N days" intent.
BothInMemoryStore::forgetandSqliteStore::forgetfixed. - Anchor 6 (KG recall across restarts returns
''): a program that
runs onlyrelate "a" to "b" as "r"(nomemorize) persists
edges toSqliteKgin Session 1; Session 2 opened the KG but
recall_twnever 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_stringdid&s[1..s.len()-1]to strip outer quotes,
contradicting its own docstring;parse_test_declpre-stripped
withs.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)
andparse_test_declpasses the raw lexeme. - Anchor 8 (id collisions when two records land in the same second):
themt_<sec>,mt_l1_<sec>,mt_l2_<sec>,cron_<sec>and
appr_<sec>ids collided on the second-precision timestamp. The
newchrono_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.
- Anchor 1 (extract_entities silent stub):
-
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 isArc<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 intotestblocks
(top-level statements are a dead syntax), the 6 semantic-arity tests
lifted as-is (their patterns only needed the now-mandatory
-> Unit),learn_preferencecalls 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 withTODO(№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 basis0xcbf29ce484222325, prime0x100000001b3), the
marker hex being the LOW 32 bits of the digest of the prompt's
UTF-8 bytes,{:08x}.DefaultHashernever 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 newtest_mock_response_golden_hexpins 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/*.expectedfiles (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
-> Tthatreturns 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 forAssign/Returnstatements); R2 — declared parameter
types (x: String) seed the inference environment, so the existing
№474 assign rule flagsx = <differently-typed builtin>with the
origin naming the DECLARATION; R3 — pattern and learnable-pattern
calls type theirletfrom 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 declaredFluidnever warns (it accepts anything by
design); labels are ERASED before comparing (String<private>
compares asString);Unknownnever 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:, default0.05) — the audit's 90/10 skew case (a model that always answers the most-frequent class scores 0.90 and clearedmin_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. Thedistill.rejectedaudit 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.
- margin)
-
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 jobops-pairscounts the
*_tw/*_vmLOGIC pairs in the shared*_ops.rsmodules
(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
renameVmDbAccess→DbAccess(thevm_method prefixes drop):
the state trait is now backend-neutral, and the interpreter lane
rides it too — theTwDbAccessadapter 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 DbAccesson 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: DbAccesswitness, 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 modulesrc/media_ops.rs(the name constants + thehandles()
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 artifactunfreeze-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
recordscripts/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).
- the GitHub step summary) with an explicit verdict per criterion:
-
Naryad №481 (issue #729; the audit 25.09 §3.9 tail — "open, io.rs:183
unchanged"): loudenv/read_fileconfig 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
newenv_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_filekeeps 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 toenv_or(name, ""); the golden examples were verified —
every executedenv()either reads a set variable or sits in a dead
branch, and the executable doc block that DID read a missing variable
(syntax.mdEntity (simple)) was migrated toenv_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.rsre-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-servewith 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_servesets the process mode to Serve
at the entrypoint (the №457cmd_serveprecedent) with the loud
strict-by-default banner — every UNMARKED thread isServeRoute
(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/callkeeps its explicitServeRouteExecGuard
(№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
(TopLevelRegistrationGuardinsideMcpServer::new, the №457
precedent) —tools/listwith the compiled №316 policy meta is
unregressed. Pinned intests/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_MOCKre-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 whenMETALOGOS_MOCK_LLM=1|trueis 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.envsidecars (the №454 posture, ADR-0169 §3.4) — five
LLM-dependent examples carry the explicitMETALOGOS_MOCK_LLM=1
now; the module docstrings and the REFERENCE/limitations rows are
synced. New BLOCKING CI jobmock-env-ssot: the retired env name
must never reappear insrc/(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_VARIABLEon the TW ident read and the VM rule path,
UNDEFINED_FUNCTIONon the VM compile refusal,TYPE_MISMATCHon
the heterogeneous concat/binary-op refusals, the TWeach-over-
non-List refusal, thelen()-argument and*_argbuiltin 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), andtry 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 resolvesdb { 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
sameenv()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=interpreteras the escape hatch. Exotic URL
expressions (neither a literal norenv("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_llmno longer hard-codes the generation ceiling —
the six"max_tokens": 1024literals 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_claudereads the same surface) → the
METALOGOS_LLM_MAX_TOKENSenv 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_reasonis read on every
provider branch INCLUDING the SSE streams (OpenAI
choices[0].finish_reason, Anthropicmessage_delta.delta.stop_reason,
Ollamadone_reason); a length truncation prints a stderr warning
naming the fix, lands in the JSONL trace as thefinish_reasonfield
(№276 honesty: absent stays absent), and is checkable from mlog via
the newllm_last_finish_reason()builtin (registry 500→501,
append-only, typedString— 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.rsis 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.tomldisallows the rawstd::fsmethods — 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 fromUnitto the TW form); the
query_rowVM params now bind TYPED through the №381convert_params
SSOT — the old stringify lane silently dropped unsupported values
(afilter_mapwith_ => 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).