Skip to content

0.32.0

Choose a tag to compare

@ShkodnikAI ShkodnikAI released this 15 Oct 10:24
· 66 commits to main since this release
08482d1

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 the private mark lived on exactly ONE registry row
    (canary_check, №661), imap_read carried all 9 fields untrusted,
    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 into smtp_send/http_post/respond with 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 are private now (the
    shape read off the impl, №650; uid/date/seen stay untrusted — the
    server metadata); (2) the BRIDGE —
    semantic::registry_private_flow_label: a row whose field-meta
    carries private seeds 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; redact stays 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_events keep the honest JSON-String spelling with
    the reason in the row comments (the stage-0 scalar vocabulary carries
    no label — a String<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 under profile 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 }}} in render() 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 a text: String template passed mlog check,
    run and serve with 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 declared Html, anything else is
    the RAW_INSERT_NOT_HTML stable-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):
    a break/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_LOOP code 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 — break on the error path instead of return,
    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 was Ok("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) and sblk_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 use return.
    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 anchor f92c61c, 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 --goal032 report-only counter
    (GREEN at the honest 0/20 start; the --gate032 N floor 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 while untyped 67 did 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.py now scans the «Honest limits»
    sections of docs/spec/*.md — every ACTIVE record (the №-id bullets)
    MUST have its mirror row in docs/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 in docs/spec/blocks.md ONLY — 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 ### Security of
    0.30.1). Spec counters (the Topics table, 33 norms) verified in sync.

  • N7 (gh#1217, PE-2, serve) — the application response headers:
    headers on HttpResponse + 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 the security_headers middleware'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. set semantics: 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
    failing register_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_render already 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 xml capability feature:
    feed_parse / sitemap_parse / xml_parse, the feed/XML family.

    Off-by-default behind xml (implies html — 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 prefers rel="alternate". sitemap_parse(xml)
    — the urlset > url and sitemapindex > sitemap shapes 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 pair examples/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 job xml-tests (the off-by-default guard +
    the feature-on suite + clippy); the feature rides portable. The
    №757 precise compensation IN THE SAME PR: decrypt and
    reflex_bpe_decode ship the verified precise String facts (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 with Struct<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 html capability feature:
    html_select / html_text / html_links, the DOM extraction family
    the tender agent needs.
    Off-by-default behind html (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.0 scraper
      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_LARGE shape — 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 reach respond_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 job html-tests (the off-by-default guard + the
      feature-on suite + clippy); the feature rides portable per 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 coarse List<...> 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.
  • N8 (gh#1218, PE-2, serve/bug) — the security_headers middleware
    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 the set_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 REAL run_test_server_with_backend HTTP 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 last opts: Struct carries
    parse_mode ("HTML" | "MarkdownV2", anything else loud), bot
    (the bot NAME — the token comes from TELEGRAM_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; without bot the legacy
    TELEGRAM_BOT_TOKEN is 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_markup rides
    ONLY the last chunk, parse_mode rides 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 legacy reply_markup (backward compat by contract). The
    new send_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_preview are 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
    opts parse_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_BASE overrides
    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.by wildcards 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's Policy::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_AGENT overrides the new default identity
    Metalogos/<version>; an explicit caller User-Agent header wins
    over both (any-case compare). METALOGOS_HTTP_MIN_INTERVAL_MS paces
    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 empty Disallow: 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 .expected would
    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_header reads a request header
    from the current route body by a case-insensitive name (the server
    injects the per-request HeaderMap with 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 carries UserInput taint (the
    semantic/audit/html-label source lists). ct_eq is 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.mlog pins the Telegram
    X-Telegram-Bot-Api-Secret-Token check 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) and metalogos-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 shared try_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, the semantic.rs
    lattice, builtins_classification.rs), the sanitize/source/sink name
    sets in them, or the render/template_render/set_header-class
    contracts must carry the sanitizer section in the PR description: the
    touched sites, the before/after for every affected source → 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.rs or ADDS a Cargo.toml feature/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 (the pe2: n5/pe2: n6 HTML/XML
    PE commits) reproduces the audit's finding verbatim — the registry
    growth plus the html/xml/url Cargo 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 by tests/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
    blocking git status --porcelain step 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_DIR roots); the CHANGELOG archive — the releases
    0.5.0→0.28.2 moved byte-exact into docs/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
    throwaway METALOGOS_DATA_DIR roots; 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_bp landed 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 with message.reasoning_content
    alongside the final message.content on 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_content yields NO text, only
    delta.content deltas 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_tokens INCLUDES the reasoning tokens) rides a
    thread-local from the provider branch into the caller's
    METALOGOS_LLM_TRACE line (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 with content: null
    (the answer cut mid-reasoning) fails with the loud
    Unexpected OpenAI response format error — 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 via METALOGOS_LLM_MODEL_{ALIAS} (the ADR-0048
    resolve_model order: env profile first, pass-through second). The
    override rides BOTH call paths — the SmartRouter model_override
    slot and the legacy backend's call_with_model (ADR-0048) — so
    glm-5.3-flash and glm-5.3 work in ONE process (the owner's
    platform decision: the primary default is glm-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 — a render("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
    tool method 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) while mlog check passed and the
    tree-walking backend returned 42 (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::patterns table (both passes walk all_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 the Expr::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
    literal 21.0 arg exposed it on the first run), and the
    arity/purity metadata now rides the same CompiledFn shape 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-typed query() 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.