Skip to content

v2.50.0

Choose a tag to compare

@github-actions github-actions released this 08 Sep 09:59
· 10 commits to main since this release

Security

  • A spoke's reply chose its own destination inside the hub, and every node routes. HTTP_Out_Node::accept_inbound() returns early for a TM_RESPONSE/TM_ERROR so a reply can self-route on the FROM breadcrumb we minted — the Remote_Link heartbeat and event-logger-nodes' Discovery_Collector both depend on it. But the REMOTE sets the bit that makes a message a reply, and that early return precedes the target refusal twelve lines below whose docblock names this exact case: "an addressed non-reply arriving while a target is set is the remote picking its own destination inside us — refused." Every node sinks into _command_interpreter and then _router (ADR-7), so the spoke's TO is routed, not decorative: on a live aggregator hub that is thirty names, _router, _command_interpreter, _fleet and two offsetlogs among them. Command execution stays blocked — the verifier is process-wide, the reply is unsigned, and Message::LOCAL cannot cross unpacked() — but a node's fill() is reachable, and Discovery_Collector::fill() merges an unsolicited payload straight into the hook picker's staging options.
    New allow_replies_to <path> verb on HTTP_Out_Node declares which destinations a reply may address; anything else is dropped with the path named. Matching is on the TO's HEAD segment, because that is what Router_Node peels: a declared head admits the deeper path a breadcrumb echo arrives on, and what the remainder means belongs to the node receiving it. Fail closed: an empty list admits nothing addressed. Remote_Link_Node declares its own name beside the target it already re-addresses in address_null_sink(), so a patron's heartbeat needs no operator action and survives a rename. This is HTTP_Out's first declared verb, so __construct() now calls auto_wire_interpreter() — without it there is no <name>:config sidecar, the verb is unreachable, and the allowlist fails closed with no way to open it. dump_config() replays each declaration so a graph round-trips.
    A per-spoke egress needs a target AND a declaration. With no target set, accept_inbound() passes an addressed message from the remote whatever it carries, so a target-less egress admits MORE than a declared one — point it at a Null, as Remote_Link_Node does for its own patron.
    Operator action required on any hand-wired HTTP_Out. A per-spoke egress created from the topology console — the shape settings-sync.tsl documents — carries no declaration and will drop its acks until one is added: cmd <name>:config allow_replies_to settings-sync and, on a hub running the event logger's hub-control, allow_replies_to discovery-collector.

Changed

  • A durable backing's record now outranks the cache TTL it was written with. Table_Node::read_through() refused an entry whose stated remaining life had run out — "a STATED lifetime that ran out is a miss, not a resurrection." That treats a cache TTL as a statement about the DATA, and it is not one: event-logger-nodes keeps a fine URL bucket two hours in memcache because the fine tier is the largest thing that schema puts in a 512MB cache, while mirroring it for twice the stats window. The refusal therefore made the durable tier useless for exactly the data whose cache lifetime is shortest — an evicted urls_h key could never be rebuilt from the fine buckets decision 17 says it derives from, which is the premise for leaving the coarse tier unmirrored at all. The entry is now SERVED either way, and warmed only when its stated remainder is positive: a spent one is not worth a cache slot, and a backing that wants an entry gone stops returning it. Stats_Store supplies the honest number — window_remaining(), seconds until the bucket leaves the RETENTION window — so a 23-hour-old hour re-warms for an hour rather than for a fresh full one.

  • The cross-language statement pin no longer pins line NUMBERS. tests/fixtures/statements/*.json are the golden the PHP StatementFrontEndParityTest and the JS parse-statements.fixture.test.js both hold their front end to. They carried each statement's line, so editing a COMMENT in a shipped .tsl failed both suites and demanded a fixture regeneration — churn standing between an author and a comment rather than a guarantee. Both halves now drop line before comparing. The parser still emits it and Shell_Node still accumulates it; it is diagnostic, and what the pin is for is the verb, its arguments and the raw text.

Fixed

  • A rotated segment was created world-readable. get_handle() wraps its fopen in umask( 0077 ); the rotation path created the next segment with touch() outside that guard, so under the usual 022 every segment after the first landed 0644 — and a firehose segment carries request URLs and environment values. Confirmed live before the fix: firehose.p0/1520.log was -rw-r--r-- while the same partition's first segment was -rw-------, and a read as another uid returned a real record.
  • /command ingress ignored stamp_message()'s documented refusal. Node::stamp_message() returns false and leaves FROM UNCHANGED when the stamped path would pass MAX_FROM_SIZE, and its docblock states the contract — "Refused two ways, and the caller must drop the message on either." The two other call sites honour it; HTTP_In_Node::dispatch() discarded the boolean and filled the message anyway, so a FROM in the eight-byte window between 1017 and 1024 bytes reached the graph unstamped, escaping the _output boundary and naming its own reply destination.
  • The command interpreter re-dispatched a TM_ERROR, so a refusal answered a refusal. Every refusal is minted TM_COMMAND|TM_ERROR addressed TO = FROM, and the interpreter's gate excluded only TM_RESPONSE — where Command_Auth::is_request_command() already excluded both. Each pass minted another, inside one PHP process.
  • Vault::is_valid_id() accepted a trailing newline. $ also matches before one, so an id that the method's own docblock requires to stay unquoted in an audit line and in a message path could split the record it was written into. Anchored with D.

Removed

  • GET_LAG, the Consumer/Tail request verb, is gone — it had no production caller. Nothing in the substrate or any consumer plugin sent it; the only callers were the REPL's request_node, the console Inspector and the tests. Lag itself is untouched: compute_lag() remains the one seam, probe_stats() and idle_since() still read it, and an operator reads the distance off wp nodes status's Behind column, which renders the probe record's distance. With the verb went Consumer_Node::handle_request(), the requests entry in its node_schema() — the key is absent now, not empty, so help Consumer and the Inspector render no request section — and Consumer_Node::fill(), whose only reason to exist was dispatching that verb. Removing the override changes no behavior: its body had become parent::fill(), and Tail_Node::forward_line()'s parent::fill() now resolves to Node::fill() directly, which is where its TM_BYTESTREAM already landed. The six tests that read the reply payload for its lag arithmetic — including the two wedged-cursor regressions from 0.15.0 — now call the compute_lag() seam instead, so that coverage survives the verb.

Documentation

  • Six documents, plus three docblock citations in the example plugin. The architecture guide now says the substrate ships TWO request verbs, GET_HEALTH and Table's GET <key>, and names Job_Worker_Node::handle_request as the canonical example; it also drops Consumer_Node::fill() from the list of overrides that inherit the FROM stamp by forwarding through parent::fill. writing-a-plugin.md moves its "shape to copy" exemplar to Job_Worker_Node and adds Table_Node as the deliberate contrast — case-SENSITIVE match, no TM_RESPONSE bit, no {verb, data} envelope, an unknown verb dropped with no reply at all. getting-started.md no longer promises a requests section for help Consumer. The three demo nodes in example-ai-newsletter cited Consumer_Node::handle_request for their reply shape and now cite Job_Worker_Node::handle_request, which matches it line for line. AGENTS.md, nodes-review and nodes-debugging follow, the last pointing an operator at wp nodes status's Behind column instead of a verb.