Repository navigation
0.32.0
Security
-
№689 (gh#1256, В44 — the R-1 audit finding; fix date 2026-10-15):
the mail ПДн (personal data) learned to SPEAK at the flow layer. Before
the fix theprivatemark lived on exactly ONE registry row
(canary_check, №661),imap_readcarried all 9 fieldsuntrusted,
and the label engine was NAME-based only — the registry field-meta
never reached the №323/№325 flow machinery: a program could send the
mail correspondence (from/to/subject/body_text/body_html/attachments)
straight intosmtp_send/http_post/respondwith ZERO findings —
the silent ПДн egress the audit's R-1 described («the machine does not
check the private → sink flow because there are almost no marks»).
The repair, RED-first (tests/naryad_689_private_field_flow.rs): (1)
the REGISTRY marks — the ImapEmail ПДн fields areprivatenow (the
shape read off the impl, №650; uid/date/seen stayuntrusted— the
server metadata); (2) the BRIDGE —
semantic::registry_private_flow_label: a row whose field-meta
carriesprivateseeds the flow layer with the type-layer secret
projection (conf=private, integrity=trusted, the ADR-0154 §2 form), so
the №325 sink gate fires the PII_EGRESS_NETWORK / PII_EGRESS_OUTPUT
classes on the let-bound AND the direct-call paths;redactstays the
only legal downward cure (№326, tested). The honest boundaries: the
bridge is deliberately NARROW (rows without a private mark — the
untrusted/integrity vocabulary — keep the old path; whole-value
fail-closed posture, the per-field precision is the stage-2 contour);
card_contacts/cal_eventskeep the honest JSON-String spelling with
the reason in the row comments (the stage-0 scalar vocabulary carries
no label — aString<private>row would erase to Unknown and regress
the typed floor; the structural typing is the №688-cycle tranche).
Metrics (the actual recount): the field-label share 46/48 = 95.83% —
unchanged; the sensitive surface 73/145 = 5034 bp — unchanged (the
marks changed values, not coverage). Affected: programs egressing the
imap_read fields into the egress sinks — they now refuse at compile
with the PII class named (advisory underprofile legacy, the №325
posture). -
№685 (gh#1252, В44 — the S-1 audit's High finding; fix date 2026-10-15):
the RAW template insert{{{ var }}}inrender()is FAIL-CLOSED now.
Before the fix the N9 raw form (gh#1219) landed its argument VERBATIM in
the page while every static layer answered «sanitized» at the render
boundary —render("Comment", "author", body.author, "text", body.text)
with{{{ text }}}in atext: Stringtemplate passedmlog check,
runandservewith zero findings: the XSS the audit demonstrated,
invisible to the HTML_INJECTION category. The repair is the audit's
prescription, three gates deep: (1) the DECLARATION gate — a template
parameter used in{{{ }}}must be declaredHtml, anything else is
theRAW_INSERT_NOT_HTMLstable-code refusal BEFORE execution on BOTH
backends (check_program + the compile_program gate block, the №673
contour shape; the code joins the №385/ADR-0169 classification, nothing
renamed); (2) the CALL-SITE coercion — a String-shaped argument into an
Html-typed parameter of a statically-known template refuses (the №662
rule-1 twin, the implicit String→Html the runtime accepted silently);
(3) the TAINT/LABEL layers —render()is now a sanitizer for the
ESCAPED positions and a CONDUCTOR for the RAW ones (the template body
decides from the program's declarations; a dynamic/unknown name is a
conservative conductor; the UserInput/LLM egress gates
OPEN_REDIRECT/HTML_INJECTION/TELEGRAM_HTML_INJECTION read the conducted
taint instead of a fake lift). The honest path is unchanged: an
Html-typed parameter built by an explicit Html producer
(render("Outer", render("Inner", s))) renders verbatim on both
backends. Affected: programs that fed String values into raw
{{{ }}}positions — they now refuse at start with the parameter and
the fix named in the message (the class-4 dictionary entry, the guard
scenario per the №653 form; the audit's «do not cut 0.31.0 before S-1»
is satisfied).template_render's raw=true trusted-author contract is
unchanged (the limitations row now tells the two contracts apart). -
№673 (gh#1200, В41 — BREAK_OUTSIDE_LOOP, the R-2 audit's two
«молча неверные» break/continue edges; fix date 2026-10-14): abreak/continue(а)
OUTSIDE any loop (№658-b) or (б) crossing the VALUE channel — an arm
body of a match/if-as-value (№658-a) — now refuses as a SEMANTIC error
BEFORE execution on BOTH backends, with the stable
BREAK_OUTSIDE_LOOPcode stamped first (the №523 shape; the code
joins the №385/ADR-0169 classification, no existing code renamed).
Before the fix both edges were the class-4 dictionary entries (№653)
the honest-limits records pinned: the TW refused at RUNTIME
(«break/continue used outside of a loop»), while the VM either
compiled the bare statement to NOTHING and continued (the audit's
import_batch shape —breakon the error path instead ofreturn,
and the code after it RAN: the recorded pre-fix VM answer was
Ok("published")) or silently ABSORBED the signal inside a
value-channel arm and ran the loop to its condition (the recorded
pre-fix VM answer wasOk("matched;matched;matched;")where the TW
refused). The repair is the audit's recommendation — a semantic gate,
not a runtime mechanism: ONE shared walker
(semantic::break_outside_loop_violations) walks every executable
body (pattern, tool method, server route, hook, on_deny handler,
test) with the loop/value-channel context model — loop bodies open
the loop context, statement-position branches preserve it (the
№658 contract the norms S-BLK-011/012 pin stays UNCHANGED — a break
nested through if/match inside a loop keeps working), value-channel
arm bodies forbid the signal outright, a loop inside an arm re-opens
the legal context for its own body. Wired into BOTH production gates:
check_program(the TW run/serve path) and — for the first time for
a semantic finding class —compile_program(the VM compile→run .mbc
path, which had no semantic gate; only THIS class is wired there, not
the whole check_program — the widening is a separate conversation).
The runtime mirrors stay untouched as backstops. Two conformance
pairs landed (the sblk_011/012 form):sblk_015_break_value_arm
(№658-a) andsblk_016_break_outside_loop(№658-b), both
STATUS: error, CODE: BREAK_OUTSIDE_LOOP— RED before the fix on
the VM (the silent answers above), green after. Honest limits:
the blocks.md №658-a/№658-b records moved to the REPAIRED form
(the №652-a sample; the edge history kept). Affected: programs that
RELIED on the silent absorption (the №653 class-4 guard scenario) —
they now refuse loudly at startup and must usereturn.
Size: M; risks: low-medium (any pre-existing program carrying the
illegal shapes now blocks at startup — that is the repair's purpose;
the legal break/continue surface is contract-pinned unchanged).
Added
-
№688 (gh#1255, В44 — the audit f92c61c S-3; the draft, the owner's
verdict pending): the 0.32 typed-source goal moves to the
FIXED-LIST closure form (ADR-0193 Proposed) — «of the 67 untyped
sensitive sources recorded at the anchorf92c61c, at least 20
closed, the ПДн-class first; the denominator is OUT of the goal».
The machine half:scripts/ci/gate_032_untyped_sources.txt(the 67
ids with the registry lines at the anchor, generated by the anchor
worktree's own №644 inventory) + the--goal032report-only counter
(GREEN at the honest 0/20 start; the--gate032 Nfloor mode exists
mechanically and wires ONLY with the owner's §5 fixation — the
gate_031 one-PR form). The X-3 fraud shape (the 0.31 goal met by the
denominator growth 66/138 → 73/145 whileuntyped 67did not move)
is closed mechanically: only closing REAL rows of the FROZEN list
moves the goal. -
№691 (gh#1258, В44 — the spec topics): the specification grew two
topics — routes.md (Topic 4: S-RT-001 the route declaration
vocabulary, S-RT-002 the direct respond* early answer, S-RT-003 the
depth-2 RESPOND_SWALLOWED refusal, S-RT-004 the raw-insert Html-only
rule, S-RT-005 the protective-header ownership — the middleware's,
not set_header's, S-RT-006 the respond_html dual contract) and
state.md (Topic 5: S-ST-001 the world_state private-by-default
materialization refusals, S-ST-002 the private label gates
materialization — not the handle flow — plus the typed device chain,
S-ST-003 the literal-key memory round-trip). NINE new conformance
pairs (both backends, the №645 runner; the №685 raw-insert pairs
referenced, not duplicated); the probes found NO cross-backend
divergence — the route/state refusals are shared-code builtins and
semantic checks. The normative core: 33 → 42 norms. The №674
mirror-scan IN SYNC (the №668 typed-coarse honesty got its №588-form
limitations row in the same PR); REFERENCE cross-references the
topics from the server and the world_state rows. -
№674 (gh#1201, В41, process/ci — the honest-boundary machine sees
the spec):honest_boundary_check.pynow scans the «Honest limits»
sections ofdocs/spec/*.md— every ACTIVE record (the №-id bullets)
MUST have its mirror row indocs/limitations.md; REPAIRED records
are exempt (the repair closed the edge — the №652-a sample). The scan
is repo-state based and runs in the SAME blocking №604 job on PRs AND
on push/main (the job's push/main branch runs the full retrospective
now instead of exiting early — no new CI cell, №535); the--diff
marker/dictionary modes stay unchanged. The gap it closes: the №659-x
/№659-y records lived indocs/spec/blocks.mdONLY — limitations.md
had no mirror (the X-1 class: honest knowledge outside the machine's
control; grep 658|659 was empty). In THIS PR the mirrors landed (the
two OPEN rows in the TW/VM Divergences section, the №588 form), the
self-test gained the hermetic collector/mirror cases, and the demo
probe (a temporary unmirrored record → exit 1 with the exact list;
removed → IN SYNC) is recorded in the naryad report. The same run
surfaced a REAL classification gap: the №673 CHANGELOG entry (the
№653 class-4 repair) sat outside### Security— moved
there in THIS PR per the №643 machine (the №629 precedent; the №658
fix of the same break/continue line landed under### Securityof
0.30.1). Spec counters (the Topics table, 33 norms) verified in sync. -
N7 (gh#1217, PE-2, serve) — the application response headers:
headersonHttpResponse+set_header(resp, name, value).
The pure value transform: it takes the response value, attaches the
allowlisted header and returns the new response; the route returns it
and the server applies the set at the conversion boundary (the
curl -i-visible result). The allowlist (case-insensitive, the
canonical spelling stored): Cache-Control, ETag, Content-Type,
Location, Content-Disposition — the PROTECTIVE headers (CSP, XCTO,
HSTS) are thesecurity_headersmiddleware's duty (N8) and cannot be
overridden from the route. The injection gate: CR, LF or NUL in
the name or the value → the typed[HTTP_HEADER_INJECTION]refusal
(the response-splitting vector) — the runtime gate on both backends
AND the static Category-A compile lane on the literals (the injection
verdict comes before the allowlist's, so a smuggled name never leaks
the allowlist's wording). The allowlist gate: any other name →
the typed[HTTP_HEADER_NOT_ALLOWED]refusal naming the offender and
the allowlist. The taint gate: a UserInput-tainted VALUE →
[HTTP_HEADER_TAINTED]at COMPILE time (the Category-A lane; the
header channel is the server's own voice — echo the client into the
escaped body, never into a response header); clean computed values
(an in-program ETag) pass.setsemantics: the same name
(case-insensitive) REPLACES, the others keep their order; a
set_header("Content-Type", ...)overrides the response's
content_type field at the boundary (the map-insert contract). The
signature ships parameterized-coarse
Struct<HttpResponse>{4 internal}WITH the field-meta (the №661
anti-dilution rule; every field is program-computed at construction).
The floors raised in the same PR (№757): general 6317 → 6326 bp,
parameterized 6893 → 6923 bp, fieldmeta 9565 → 9574 bp; precise
4307 bp (the dilution absorbs within the 4302 floor on this branch).
REFERENCE + the classification regenerated (519 → 520 builtins);
registry_arity re-blessed; the gate facts updated. 11 tests (9
integration + 2 server-boundary), RED first. Size: M. Risks: the
allowlist is deliberately narrow (a need outside the five names is an
explicit ADR conversation, not a silent widen). -
N9 (gh#1219, PE-2, compiler/bug) —
render()and| safe: no more
silent escaping. The owner's finding:render()always escaped,
the Jinja-style{{ var | safe }}was NEVER implemented (the №115
substitution left the placeholder VERBATIM in the output, the №86
engine silently rendered it empty), and the composition example
pinned the silent mismatch with an EMPTY .expected. The fix combines
the naryad's menu options 1+2+3. (1) The filter pipe is a LOUD
refusal — a compile error on BOTH backends (the №250 compiler
registration gate and the interpreter declaration walk share the
failingregister_template) and a loud parse refusal in
template_render, each naming the supported raw form; a pipe inside
a condition string literal ({{#if x == "a|b"}}) stays legal — the
construct grammar is not a filter. (2)render()gains the RAW
insert — the triple-brace form{{{ var }}}, the same contract
template_renderalready had; safe-by-construction: the parameters
are program-provided, the egress gates still apply downstream. (3)
The composition example (p6_template_composition) actually renders
now — the data rides the ESCAPED default ({{ title }}, the
script-neutralization is visible in the .expected), the trusted
program-built fragment rides raw ({{{ content }}}); the empty
.expected (the pin of the silent empty render) is replaced by the
real output. REFERENCE §4.14 documents the raw form and the refusal. -
N6 (gh#1216, PE-2, ADR-0192) — the
xmlcapability feature:
feed_parse/sitemap_parse/xml_parse, the feed/XML family.
Off-by-default behindxml(implieshtml— the shared text/limit
helpers; xml5ever 0.18, the same MIT/Apache-2.0 servo stack, promoted
from the transitive markup5ever_rcdom dep to a direct optional). The
registry surface is feature-INDEPENDENT: without the feature the
three handlers refuse LOUDLY naming ADR-0192.feed_parse(xml)—
RSS 2.0 (rss > channel > item) and Atom 1.0 (feed > entry) detected
by the root element; every item →List<FeedItem>{title, link, published, summary, id}; a missing field is the EMPTY string
(fail-closed items — one ragged entry never kills the batch): pubDate
falls back to dc:date, published to updated, summary to content, guid
to link; the Atom link prefersrel="alternate".sitemap_parse(xml)
— theurlset > urlandsitemapindex > sitemapshapes uniformly →
List<SitemapEntry>{loc, lastmod}(lastmod often absent → "").
xml_parse(xml)— the general tree →Struct<XmlNode>{tag, attrs, text, children}: the local name is CASE-SENSITIVE (XML is), attrs is
a Dict (namespace DECLARATIONS are not attributes — the XML5 grammar
carries them out-of-band), text is the DIRECT text (normalized,
CDATA contributes), children recurses. Security: DTD and external
entities are NEVER expanded — the XML5 grammar has no custom-entity
mechanism, so XXE degrades to literal text and billion-laughs stays
bounded (both pinned by explicit negative tests). The loud limits:
input ≤ 5 MiB, result ≤ 10 000 nodes, tree depth ≤ 256 (the recursive
build must not blow the stack) — all loud, never a silent truncate.
EVERY result string carries the untrusted taint (the N2/N5 source
lists). The golden pairexamples/n6_feed_parse.mlog(+.expected)is
byte-identical on TW and VM; the default golden sweeps skip it (the
№373 parity gate: the xml-gate list joins the frozen capability
classes). New blocking CI jobxml-tests(the off-by-default guard +
the feature-on suite + clippy); the feature ridesportable. The
№757 precise compensation IN THE SAME PR:decryptand
reflex_bpe_decodeship the verified preciseStringfacts (227/525
= 4323 bp; the floor 4302 → 4323), the parameterized share 76/108 =
7037 bp (floor 6893 → 7037), the general 336/525 = 6400 bp, the
field-meta 45/47 withStruct<XmlNode>carrying the honest
all-untrusted labels. REFERENCE + the classification regenerated (522
→ 525 builtins). 15 tests (14 feature-on + 1 off-feature refusal),
RED first. Size: M. Risks: the RSS/Atom real-world variance is wider
than the two fixtures (the fail-closed item shape absorbs the ragged
forms); XXE hardening is by-grammar + negative-tested. -
N5 (gh#1215, PE-2, ADR-0192) — the
htmlcapability feature:
html_select/html_text/html_links, the DOM extraction family
the tender agent needs. Off-by-default behindhtml(html5ever 0.27- markup5ever_rcdom 0.3 + url 2 — MIT/Apache-2.0/Unicode-3.0 only, the
№518 license gate verified by the N5-pre probe; the MPL-2.0scraper
stack rejected). The registry surface is feature-INDEPENDENT (the N559
rhythm): without the feature the three handlers refuse LOUDLY naming
ADR-0192, never silently.html_select(html, css)returns
List<HtmlMatch>{tag, text, attrs, html}— the selector subset is
tag/*/.class/#id/[attr]/[attr=value]/descendant/direct-child/
comma (anything else a loud parse error naming the subset and the
position); document order; attrs is a Dict.html_text(html, css?)
returns the whitespace-normalized text — script/style subtrees NEVER
contribute.html_links(html, base_url)returns
List<HtmlLink>{href, text}— hrefs are ABSOLUTE (the WHATWG
resolution via the url crate), only http/https survive,javascript:
and every other scheme is DROPPED by contract, never an error. The
loud limits (fail-loud discipline): input ≤ 5 MiB
(HTML_INPUT_TOO_LARGEshape — the error names the limit), result ≤
10 000 nodes (never a silent truncate). Broken-HTML tolerance is
html5ever's spec error-recovery. EVERY result string carries the
untrusted taint — the audit/semantic/html-label source lists mirror
request_header(N2): scraped HTML/text must not reachrespond_html
without the escape/validate step. The golden pair
examples/n5_html_tender.mlog(+.expected)(the synthetic Cyrillic
tender page) is byte-identical on TW and VM (the feature-on contract
test H8); the default golden sweeps skip it (the html-gate list, the
№373 parity gate re-justified: three sanctioned exclusion classes).
New blocking CI jobhtml-tests(the off-by-default guard + the
feature-on suite + clippy); the feature ridesportableper the
Cargo.toml feature-block rule. REFERENCE + the classification
regenerated (519 → 522 builtins); the precise-share fact 4315 → 4310
bp (html_text ships precise String; the two coarseList<...>rows
dilute — the floor raise is the №757 follow-up), the parameterized
fact 6893 → 6952 bp. 15 tests (14 feature-on + 1 off-feature
refusal), RED first. Size: L. Risks: the minimal selector engine is
deliberately NOT a CSS engine (the unsupported syntax is loud);
fuzzing on broken HTML rides the existing fuzz-smoke lane.
- markup5ever_rcdom 0.3 + url 2 — MIT/Apache-2.0/Unicode-3.0 only, the
-
N8 (gh#1218, PE-2, serve/bug) — the
security_headersmiddleware
actually applies the protective headers. The owner's finding: the
declaration was VALIDATED (the semantic name check + the "no
security_headers" recommendation warning) but never APPLIED — a plain
GET showed no CSP, no XCTO, no HSTS. The fix: the middleware applies
the three protective headers to EVERY response — the route responses,
the axum fallback 404 and the direct-dispatch 404 (the OWASP
every-response shape):Content-Security-Policy: default-src 'self',
X-Content-Type-Options: nosniff,
Strict-Transport-Security: max-age=31536000; includeSubDomains. The
middleware OWNS the set: unconditional inserts, and theset_header
builtin (N7) does not carry these names — a route cannot override the
protective set (the N7 allowlist is the boundary: application headers
belong to the route, protective headers belong to the middleware).
The opt-in contract is byte-exact: WITHOUT the middleware in the list
the headers are ABSENT (pinned by the negative legs). The wire-level
contract (the №892 precedent — the defect lived on the wire, not at
the builtin): a REALrun_test_server_with_backendHTTP round-trip,
BOTH serve backends × with/without the middleware × route/404 = 4
tests, RED first (the with-middleware legs failed on the missing
headers before the fix). REFERENCE documents the applied set + the
ownership boundary. Size: S/M. Risks: the CSP baseline
(default-src 'self') is strict by design — a route serving inline
scripts/styles needs an explicit config conversation, not a silent
exception. -
N3 (gh#1212, PE-1) — the send_message opts + send_photo: the
Telegram delivery family grows up.send_message(chat_id, text, reply_markup?, opts?)— the optional lastopts: Structcarries
parse_mode("HTML"|"MarkdownV2", anything else loud),bot
(the bot NAME — the token comes fromTELEGRAM_BOT_TOKEN_<NAME>, the
name uppercased; an explicitly named bot with no token env is a loud
error naming the ENV VAR, never a value; withoutbotthe legacy
TELEGRAM_BOT_TOKENis read),disable_preview(maps to the Telegram
disable_web_page_preview),chunk(default TRUE — the text routes
through text_chunk, the "paragraph" strategy, budget 4000, overlap 0;
every chunk leaves as its own sendMessage call,reply_markuprides
ONLY the last chunk,parse_moderides every chunk; a text without
paragraph boundaries degrades to the hard 4000-char windows), and
retry{max_retries, base_delay}(base_delay in milliseconds, the
METALOGOS_HTTP_MIN_INTERVAL_MS convention; a 429 honors the server's
retry_after, a 5xx backs off base_delay·2^attempt, BOTH capped at
30 s —metalogos::builtins::TELEGRAM_RETRY_CAP_MS; up to
max_retries (default 3) retries after the initial call; other 4xx are
loud immediately). A 3-argument Struct WITHOUT any known opts key
stays the legacyreply_markup(backward compat by contract). The
newsend_photo(chat_id, photo, caption?, opts?)— the photo is an
HTTPS URL or a file_id (the API passthrough), the caption is refused
loud beyond 1024 characters BEFORE any wire traffic,chunk/
disable_previeware documented no-ops for a photo. The secret
discipline is family-wide: every diagnostic is token-redacted (the
reqwest URL Display and a hostile endpoint's echo body carry no token
fragments; send_message / send_photo / answer_callback_query /
edit_message_text / send_document all redact). The compile gate
TELEGRAM_HTML_INJECTION(the HTML_INJECTION analogy the naryad
pins): untrusted text into the text/caption position with the literal
optsparse_mode: "HTML"is a Category-A compile error naming the
fix (escape_html) — opts-aware, both taint lanes, ordered BEFORE the
№325 sink clearance so the shared site names the scenario; the
lattice stays the general gate (MarkdownV2/ opts-less keep
UNTRUSTED_EGRESS_NETWORK).METALOGOS_TELEGRAM_API_BASEoverrides
the API origin — the test/proxy seam mirroring
METALOGOS_OPENAI_BASE_URL (№12). 30 tests
(tests/naryad_n3_send_opts.rs) over a canned-response Telegram mock
(bind → env-lock → serve, early-exit after the expected request
count — the parallel-safe mock discipline): the wire shapes, the
chunk boundaries + the last-chunk markup, the 429/5xx retry budgets
and the 30 s cap, the never-leaks pack, the bot-name env resolution,
the loud opts validation, the caption limit boundary (1024/1025),
the TW↔VM parity leg, and the compile-gate pack. RED first (23/30
behavioral failures on main; the helper-level test did not compile —
no such API). Golden pairs:telegram_send_opts.mlog(the no-token
Unit stubs with opts accepted) +
err_send_photo_caption.mlog(the off-server caption refusal).
REFERENCE regenerated (518 → 519 builtins), the classification row
added, the arity export re-blessed, the gate facts synced
(fact_precise_share_bp 4324 → 4315 — the precise share is
count-stable over a bigger denominator, the goal ratchet 3000 bp
untouched). Size: M/L (the largest naryad of PE-1, per the issue);
risks: medium — the chunk=true default is an INTENDED behavior
change for >4000-char texts (documented), the opts recognition is
key-based in the 3-argument position (a Struct without opts keys is
a reply_markup — mixed structs are loud), and chunk boundaries may
carry a leading paragraph separator (text_chunk keeps separators for
lossless reassembly — delivered as-is, v1-min). -
N4 (gh#1213, PE-1) — the outgoing-HTTP hardening: the host
allowlist, the User-Agent identity, the per-host pacing, the robots
politeness gate. All four egress builtins (http_get,
http_post,http_download,http_post_multipart) now sit behind
the env-driven gate stack, read at CALL time (the №130
ssrf_guard_enabled()convention).METALOGOS_HTTP_ALLOW_HOSTS
(comma list,*.example.bywildcards anchored at the dot,
case-insensitive, whitespace-tolerant) refuses any non-listed host
with the typed[HTTP_HOST_NOT_ALLOWED]BEFORE any DNS lookup or
connect; per redirect hop by construction — №261'sPolicy::none
means every hop is a new program-level builtin call, so the gate
re-runs on each hop (the №261 SSRF-pin-per-hop argument replayed for
the allowlist; unset/empty = gate off, backward compatible).
METALOGOS_HTTP_USER_AGENToverrides the new default identity
Metalogos/<version>; an explicit callerUser-Agentheader wins
over both (any-case compare).METALOGOS_HTTP_MIN_INTERVAL_MSpaces
the requests per host:port bucket (different endpoints never wait on
each other; retries are paced too). The{robots: true}call option
(http_get / http_download — retrieval is crawl semantics, a POST is
an action) fetches robots.txt once per host over the same gate
stack, caches the parsed rules for exactly 24 h
(metalogos::builtins::ROBOTS_TTL), enforces the minimal
User-agent: *ruleset with the RFC 9309 longest-match verdict (a
tie goes to Allow; the emptyDisallow:allows everything; no
*/$inside rule paths — the documented minimal scope) and
refuses a denied path with the typed[HTTP_ROBOTS_DENIED]; a
missing (4xx) or unreachable robots file fails OPEN and the empty
ruleset is cached the same way (an unreachable host is not
re-probed on every call). Two new whitelisted origin-stamped codes
(HTTP_HOST_NOT_ALLOWED,HTTP_ROBOTS_DENIED) join the №592/№385
try{}-branchable taxonomy. 18 tests:
tests/naryad_n4_http_hardening.rs— the matching table, the
typed-before-connect refusals on all four builtins, the TW↔VM
refusal parity, the UA default/override/caller-wins over a live
mock, the per-host pacing (and its absence across hosts), the
robots allow/deny/cache-24h/fail-open/longest-match pack. RED
first (the refusal tests observed the request GO THROUGH on main).
No golden pair: the surface is env-driven — a.expectedwould
pin nothing off-server («где применимо» per the DOD). Size: M;
risks: medium-low — no existing shape changed (the gates are no-ops
with the env unset, verified by the №130/№261/№71/№592/№76/№260
regression pack green), the visible deltas are the new UA header on
every request and the pacing sleeps when the interval is configured. -
N2 (gh#1211, PE-1) —
request_header(name)+ct_eq(a, b): the
webhook-secret builtins:request_headerreads a request header
from the current route body by a case-insensitive name (the server
injects the per-requestHeaderMapwith lowercased names on BOTH
backends; multi-value headers join with", "); an absent header or
a missing route context answers the empty string — the query_param
№40 parity contract, so a secret check fails CLOSED off the server
instead of erroring. The result carriesUserInputtaint (the
semantic/audit/html-label source lists).ct_eqis the
constant-time string equality for secret comparisons: the XOR/OR
accumulator never short-circuits on a differing byte (no==
prefix-timing leak; the length check leaves only public metadata out
of the accumulator path). The verdict restores INTEGRITY for
decisions (comparing an untrusted token against the expected secret
IS the validate-before-decide step the №327 gate asks for) while its
CONFIDENTIALITY follows the compared values (a verdict over private
material stays private for egress). The canonical example
examples/telegram_webhook_secret.mlogpins the Telegram
X-Telegram-Bot-Api-Secret-Tokencheck shape. REFERENCE regenerated
(curated §4 crypto/web rows + the generated index, 516 → 518).
16 tests:tests/n2_request_header_ct_eq.rs(10 — the off-server
shape, the ct_eq correctness table, the loud arg errors, TW↔VM
parity) andmetalogos-server/tests/naryad_n2_request_header.rs
(6 — the live-header route surface on both backends with the strict
TW↔VM cross-check). RED first. Size: M; risks: low-medium — two new
builtins (no existing surface touched), the taint-lane additions
mirror the query_param source treatment verbatim, and the ct_eq
sanitizer semantics are the verify_password mirror (integrity
restored, confidentiality preserved). -
№670 (gh#1185) — the specification topic 3 «Errors and the try
result» (docs/spec/errors.md — docs-only, the behavior is RECORDED,
not changed): six norms S-ERR-001…006 pin the existing try machinery
on BOTH backends per the №645 protocol — the TryResult shape
{ok, value, error} (the sharedtry_result_struct), the caught
runtime refusal (ok=false, value=Unit, execution continues), the
TryError{code, message} struct, the №385 classification contract (the
whitelisted position-0 origin stamp, else the honest RUNTIME_ERROR —
mid-message markers are content, not stamps), the semantic-blocking
boundary (an undefined name refuses BEFORE execution regardless of
try), and the first-class inner-try value (nesting). Seven conformance
pairs (tests/conformance/serr_*) ride the existing
conformance (blocking)job (the №535 rule — the cage does not grow);
the runner demands the cross-backend agreement first. The README
Topics table is synced with the actual fact (topic 2 → S-BLK-001…014 —
the №658/№659 norms; the normative core 23 → 33). The topic-3 probe
found NO cross-backend divergence (the shape and the classification
are shared-code parity by construction); the stable refusal codes are
referenced, never redefined (№385/ADR-0169). -
**№640 (gh#1123, В36, process/adr) — ADR-0188 «The Template
Semantics Composition» (docs-only, the ledger gh#967 §7 lane plan):
the compile-time port re-priced, the phased recommendation —
Proposed at the merge (bddc4cc5, ADR 179 → 180) and
Accepted — option (б) by the owner's verdict 2026-10-08
(gh#1123); the option (б) compile-time checker landed by №662
(gh#1163), the pipe syntax stays the documented gap (№115).
Changed
-
№686 (gh#1253, В44 — the audit f92c61c S-1/S-2): the risk-review
checklist grew item 10 — SANITIZERS, SOURCES, AND SINKS. A PR changing
the taint/label sites (audit.rs,html_label.rs, thesemantic.rs
lattice,builtins_classification.rs), the sanitize/source/sink name
sets in them, or therender/template_render/set_header-class
contracts must carry the sanitizer section in the PR description: the
touched sites, the before/after for every affectedsource → sink
path, and the regression pair (or the justification of its absence).
The reminder is mechanical — the risk-review report prints the item-10
section whenever the diff touches a site file, and a site hit triggers
the report on its own (no new CI job; the existing advisory job grew
the scan, the №604-class form; the guard's own check is a fixture
self-test pinned by the CI-side driver
tests/naryad_686_sanitizer_guard.rs). -
№687 (gh#1254, В44 — the audit f92c61c S-2): the naryad-class
counter counts the commits OUTSIDE the naryads — a commit without a
recognized naryad number (Naryad №…/Наряд №…) that touches
src/builtins/registry.rsor ADDS aCargo.tomlfeature/dependency
key is a red-line finding the counter cannot «not notice»; the window
verdict FAILS (the three-line report: the naryad quota, the off-naryad
findings with sha + files, the combined verdict — the dispatch--line
format unchanged). Docs-only and test-only diffs outside naryads stay
legal (they grow no surface); a dependency version bump modifies, it
does not add. The live S-2 window (thepe2: n5/pe2: n6HTML/XML
PE commits) reproduces the audit's finding verbatim — the registry
growth plus thehtml/xml/urlCargo keys, exit 1. The rule lives
in AGENTS.md §8 (the agent entry point); the guard's own check is a
fixture self-test pinned bytests/naryad_687_off_naryad_quota.rs. -
№690 (gh#1257, В44 — the audit f92c61c D5): the README CI badge
is the REAL GitHub workflow badge now
(actions/workflows/ci.yml/badge.svg?branch=main— it turns RED
when CI fails; the static brightgreen shields picture never did, and
its count had already drifted from the machine count). The №676 D5
honesty survives: the blocking-job count still RENDERS from ci.yml
into the text beside the badge (badge_sync.py— the machine
renders, the human never hand-edits; the SSOT count 44 pinned by the
render/verify round-trip). The badge is theme-independent by
construction (white text on opaque gradients — legible on both the
light and the dark README). The README stays at 149 lines (the D2
gate). The live proof of the D5 point: the badge currently shows
FAILING on main — the honest-boundary retrospective (the push/main
scope that №685 turned on) flags the three boundary rows pending the
ADR-0179 §6 release-time label sync — the badge surfaces what the
static picture hid. -
№676 (gh#1203, В41, docs/hygiene — the audit 7c3456b §4 checklist,
all 11 rows): the repo-tree cleanliness is now machine-enforced — a
blockinggit status --porcelainstep after the Library tests AND the
Integration tests (no new job; on its first full-suite run it caught
two committed repo-tree writers, both lanes moved to throwaway
METALOGOS_DATA_DIRroots); the CHANGELOG archive — the releases
0.5.0→0.28.2 moved byte-exact intodocs/changelog/ CHANGELOG-2025-06_2026-10.md(42 sections, no record lost; the live
root keeps [Unreleased] + 0.29.0/0.30.0/0.30.1 with their Security
records; 727 KB → 57 KB); the README front-door reworked (Quick Start
into the first 60 lines, 149 lines total, the feature-gate table →
docs/feature-gates.md, the Newcomer Contract deep-dive → the link with
the machine-pinned §3/§5 claims kept); the repo root is 8 .md files
(AI_USAGE/MEMORY_ROADMAP/FEATURE_INTAKE/GOVERNANCE → docs/, the
byte-identical GEMINI.md duplicate deduped); the generated
naryad-number stamp left the README (the generator no longer stamps
it, badge-sync greps it); the CI badge is RENDERED from ci.yml
(28 → 44 — the static badge had drifted); the MSRV claims synced
(1.93.1 everywhere);frames/and the leaked data/ fixtures left the
tracked tree; the repo-writing test suites run in the №387 sandbox
form (the №402 ledger export and the №394 tool write now land under
throwawayMETALOGOS_DATA_DIRroots; the №337 GENERATE leg's latent
shared-scratch race with the CAPTURE leg separated). -
№660 §5 (gh#1161 — the owner's verdict 2026-10-13) — the 0.31
release gate OWNER-FIXED (the §5 mechanical transition, the №630
shape): the OD divergence (ADR-0191 §3/§4) closed by the variant
(а) — the live-fact fixation 66/138 = 4782 bp (the №644 machine
read on the fixation anchor 34a6370 agrees exactly; the movement
№668/№669 landed before the fixation, so the §5 read starts MET by
the reached number and the gate holds as the ONLY-UP ratchet — the
X-3 posture recorded, not hidden). ONE PR: gate_031_goals.txt
owner_fixed: true+goal_sensitive_surface_bp: 4782, the fact key
fact_sensitive_surface_bplanded WITH its checker (№525 — synced
blocking by sync_gate_facts.py), the DEFAULT unfreeze target switched
0.30 → 0.31 (the 0.31 read = the 0.30 v2 form verbatim + the
sensitive-surface goal, fail-closed; the 0.28/0.29/0.30/legacy
targets stay explicit — no record deleted), ADR-0191 → Accepted
(§7). The 4130 / 4420–4493 draft variants retired (the history kept
verbatim in the record); the 4 eternal honest refusals (query /
query_row / query_scalar / request_body) remain the separate
owner-decided dictionary-expansion line. The landing re-bless
(№757/№613): the §5 merge brought the PE-2 platform-enabler wave
(n5–n9) and the В43 nuclear №684 onto the same tree — every new
sensitive source landed fully covered, the machine read rose
66/138 = 4782 bp → 73/145 = 5034 bp (the denominator moved by new
sources only — Internal 102 → 109, the definition unchanged); the
fact twin and the floor raised in the SAME PR (5034), the
OWNER-FIXED goal line stays 4782 until the owner's later §5
decision — the ratchet reads 5034 ≥ 4782: MET. -
N13 (gh#1221, platform-enabler) — reasoning_content: the thinking
block never leaves the contour, the usage does. GLM reasoning
models (thinking enabled) answer withmessage.reasoning_content
alongside the finalmessage.contenton the OpenAI-compatible wire.
The purity contract is now pinned on all three paths — the
non-stream legacy parse, the SmartRouter parse, and the streaming
delta extraction (delta.reasoning_contentyields NO text, only
delta.contentdeltas flow): the user sees ONLY the final content,
never a reasoning fragment. The usage accounting gap is CLOSED: the
legacy (no-llm config) path used to drop the provider's usage
block on the floor — the trace line carried no token counts at all.
Now the usage the provider reported for THIS call
(prompt_tokens/completion_tokens; per the GLM/OpenAI contract
completion_tokensINCLUDES the reasoning tokens) rides a
thread-local from the provider branch into the caller's
METALOGOS_LLM_TRACEline (gen_ai.usage.input_tokens/
gen_ai.usage.output_tokens), cleared at every RealLlm entry and
consumed once — a stale value can never be attributed to a later
call; a response without a usage block stays honestly absent.
Verified-loud edge: a reasoning-only response withcontent: null
(the answer cut mid-reasoning) fails with the loud
Unexpected OpenAI response formaterror — never a silent empty
string. A plain response without a reasoning block is unchanged. -
N12 (gh#1220, platform-enabler) — per-call model choice on
call_llm: the builtin grows an optional third String argument —
call_llm(prompt, input, model)— naming the model for THIS call:
a direct provider model name ("glm-5.3") or a named profile alias
resolved viaMETALOGOS_LLM_MODEL_{ALIAS}(the ADR-0048
resolve_modelorder: env profile first, pass-through second). The
override rides BOTH call paths — the SmartRoutermodel_override
slot and the legacy backend'scall_with_model(ADR-0048) — so
glm-5.3-flashandglm-5.3work in ONE process (the owner's
platform decision: the primary default isglm-5.3-flash). The
legacy-path LLM trace names the model THIS call actually asked for
(override first, env default second — honest per-call data, №276).
Two-argument calls are byte-for-byte unchanged (the default leg is
pinned by a test). A non-String third argument is a loud
argument-type error, never a silent ignore. Registry arity 1..2 →
1..3; REFERENCE §4.5 + the env-var table (METALOGOS_LLM_PROVIDER /
METALOGOS_LLM_MODEL / METALOGOS_LLM_MODEL_{ALIAS}) updated. -
№669 (gh#1184) — the typing movement 0.31 №1 (docs-adjacent,
registry-locked; the behavior unchanged): the forecast/graph
untyped package — 4 of 6 registry rows get honest signatures against
the verified handler facts (forecast_state Struct{10
fields — the prov-block metadata, 8 internal + horizon/label
untrusted}, forecast_points Struct{4 internal},
graph_neighbors List, graph_query List); 2
honest refusals (the №650 form): forecast_next — the opaque
ForecastHandle (the stage-0 OpaqueKind has NO Forecast kind; a
fictitious spelling is forbidden — the dictionary line is a separate
owner decision), graph_path — the heterogeneous List (PathNode vs
the PathError singleton). The sequential recount on the post-№668
base: typed 326/516 = 6317 bp, parameterized 71/103 = 6893 bp,
field-label 44/46 = 9565 bp, sensitive 66/138 = 4782 bp (only-up,
№757); the compiled pins 322/68/99/43; the
fact_parameterized_share_bp twin 6893 (the gate-facts-sync). -
№668 (gh#1183) — the Q-2 movement №2 «memory → recognition»
(docs-adjacent, registry-locked; the behavior unchanged): 8 of 9
gate_031 rows closed against the verified handler facts — the four
typed-coarse rows parameterized (human_personas List,
human_recall List, check_reminders List,
list_reminders List), semantic_search List,
mtree_retrieve List, mtree_stats
Struct{7 internal} + mtree_summarize
Struct{7 internal} (both WITH the field-meta — the
anti-dilution rule); the ninth row (world_state) is the honest
OPAQUE WorldState handle (№355/ADR-0159 — not a scalar, not a
parameterized List/Struct; the dictionary line is a separate owner
decision). Sensitive 54/138 = 3913 → 62/138 = 4492 bp (only-up);
the floors in the same PR. -
№662 (gh#1163) — the ADR-0188 option (б) compile-time port (the
behavior change, fail-closed; lands in the 0.31.0 semantics — NOT in
the 0.30.1 train): the SHARED semantic checker refuses two
late-failure classes at CHECK time on BOTH backends (the owner's
verdict gh#1123, 2026-10-08; the parity postulate — the checker is the
backend-neutral front door): (1) the IMPLICIT String→opaque coercion —
a String-shaped value (a literal or a+chain) flowing into a
target declared with an opaque type (Html/…) at an assignment
(entity initializer) or an argument position; the explicit path (an
Html producer —render()) stays legal (№114 resolved); (2) the
unknown template names — arender("Name", …)whose
statically-resolvable name is absent from the program's template
declarations (the №250 registration set; the request-time 500 moves
left to check time; ONLY a string-literal name is statically
resolvable — a bare Ident is a runtime-evaluated name (the CI-verified
ok_448 leak-flow shape: render(raw) passes the variable holding the
name) and every non-literal form stays the runtime's backstop). The
corpus is clean (the recursive dry-run inventory: 370 .mlog files
across examples/compat/tests, 0 refusals); the diagnostics move left
for NEW code only. The pipe syntax stays runtime-absent (№115 — the
documented gap, option (а) is later). -
№637 (gh#1120, В36, types) — the parameterized movement 0.30
№3 (the registry surface): the PDF contour, 21 verified rows (the
20 Struct rows WITH field_meta — the anti-dilution rule), the
parameterized share 34/91 = 3736 bp → 55/91 = 6043 bp — the 0.30
owner-fixed goal (≥ 5000 bp) REACHED; the floors raised in the
same PR (the parameterized baseline 6043, PARAM_FLOOR 52).
(e7894a1e) -
№638 (gh#1121, В36, types) — the field-labels №2 (the registry
surface): the 15 existing parameterized Struct rows labeled per
the №627 table read off the handlers, the field-label metric
5750 → 9500 bp (38/40 = 95%) (FIELDMETA_FLOOR 37).
(48301708)
Fixed
- №684 (gh#1244, В43 nuclear — VM tool-method resolution): a
toolmethod call (calc.double(21.0)) now COMPILES and RUNS on
the VM — before the fix the bytecode pipeline refused every
qualified tool call (error: compile: qualified call 'calc.double()' — not found) whilemlog checkpassed and the
tree-walking backend returned42(the В42 no-progress flag,
gh#1233 comment 6146003277). The compiler now follows ADR-0054
Implementation Strategy verbatim: tool methods compile as REGULAR
patterns under qualified keys (tool.method) into the existing
Program::patternstable (both passes walkall_decls
top-to-bottom, so the №415 positional contract holds; no new
opcode, the .mbc schema is untouched — a tool call never compiled
before, so old files are unaffected), and theExpr::QualifiedCall
resolve gains the tool-namespace branch that mirrors the
interpreter's dispatch order EXACTLY (execution.rs): runtime-op
intercepts and plain builtins resolve BY FUNCTION NAME first
(plain builtins shadow tool methods on TW — pinned by contract),
then the qualified method, then a loud
compile: undefined method '<m>' in tool '<t>'— never a
fallthrough to flat patterns / the global-builtin fallback (TW
would not reach those either). Two latent compiler holes closed on
the way: the QualifiedCall arm never emitted its ARGUMENTS (no
example in examples/ used qualified calls, so the gap was
unreachable until the tool branch made them reachable — the flag's
literal21.0arg exposed it on the first run), and the
arity/purity metadata now rides the sameCompiledFnshape as
ordinary patterns. 13 both-backend contracts in
tests/tool_abstraction_contract.rs(the 9 ADR-0054 contracts get
their VM leg + the flag's minimal repro as the golden pair
42 == 42+ the shadowing/intercept mirrors); crosscheck,
naryad_373_parity_gate, vm_golden, golden, conformance and the
866-test lib suite stay green. Known pre-existing residue (out of
scope, flagged): the wrong-typedquery()diagnostic suffix
diverges between the TW and VM db-op dispatch paths
(db_ops.rs:139 vs :702) — reachable through plain calls long
before this fix; behavioral parity is contract-pinned. Size: M;
risks: low (the tool branch is gated on declared tool names; the
resolution order mirrors the interpreter, so no new backend
divergence). Unlocks the showcase App C (№682) after the next
pinned release.