Skip to content

Aleph v26.9.1

Latest

Choose a tag to compare

@github-actions github-actions released this 01 Sep 07:54
· 2393 commits to main since this release

Five days, 549 commits, 822 files, +99.7k/-9.0k — of which +58.6k/-8.0k is
source, measured against v26.8.27 and excluding the committed Panel dist.
Two features and one very large sweep. First, the terminal becomes a
first-class surface
: a VT emulator lives in the gateway, holds the screen
server-side, and ships dirty-row patches to a canvas2d grid in the Panel —
which means the wire carries a screen, not a byte stream, and every reader
agrees on what geometry a frame describes. Second, a project room can be
bound to a channel conversation
, so a Feishu/Slack thread and an Aleph room
are one place with one roster and one scope. Third, and largest by far: an
audit sweep across every subsystem — a 40-defect hardening round against
the openclaw reference, eight rounds of per-module logic audits, and 22
numbered fix rounds. Underneath the sweep runs one repeated shape: a predicate
that answered "I could not tell" with "no", and the fix is almost never at the
call site — it is at the type that let the two answers look alike.

Added

  • An embedded terminal, end to end. A character grid with wide-glyph and
    wrap semantics, straddled-spacer repair, scrollback with correct eviction,
    the alternate screen buffer, resize, cursor positioning, erase, C0 and SGR —
    parsed against a pinned vte API surface. The screen is held server-side and
    published as diffs on a 16 ms cadence; the raw byte topic is retired.
    pty.attach replays into a client that does gap detection, stale-frame
    discard and attach buffering. The Panel gets a Terminal nav mode, a canvas2d
    grid renderer, keyboard encoding, and a font stack whose shipped default
    actually has icon glyphs.
  • The terminal's authority is scoped, not ambient. pty.screen / pty.exit
    delivery and the five addressed methods are gated on session ownership; spawn
    cwd is jailed to the registered workspaces (pinned to the resolver that reads
    configuration, not a second copy); geometry that would abort the daemon is
    refused; a [policies.terminal] session gate ships live and default-on, and
    the switch's writer is gated and recorded.
  • Project rooms bind to channel conversations. projects.channel.bind /
    unbind / list with one protocol author for the wire shapes, a
    roster-gated channel-binding arm in room claiming, session_store::rescope
    taking a project id rather than a rendered scope, every agent's row rescoped
    for a bound conversation, and the room scope carried across a harness spawn.
    Reachable from aleph projects on the CLI, from room settings in the Panel,
    and from project_manage bind_workspace. Covered by a real-machine fixture
    (qa/rooms_channel_bind).
  • grep and find are tools. Content search and filename discovery each
    get their own verb over the shared .gitignore-aware walk, and bash stops
    describing the web-search tool in its own description.
  • One implementation of "how do I search." There were two. Now each provider
    declares what it can express and the claim is pinned to the wire; the
    request's capability bits are a function of the request; an explicit provider
    is honoured; the freshness vocabulary and the "what this result set is
    missing" sentences each have exactly one owner; include/exclude domain lists
    reach the backends that have them; one call can fan out to several backends.
    The search tool exposes the knobs that already existed and stops discarding
    four fields, and the Panel's provider cards are derived from the protocol's
    single list of which backends exist.
  • A history window that loads earlier across clients, with the same
    vocabulary in the protocol, the CLI, the TUI and the Panel.
  • A repair pass for on-disk session damage. Two already-fixed writer bugs
    left millisecond last_active_at stamps that outranked every healthy session
    in the descending sort, and torn metadata.json documents that made
    list_sessions warn on every poll. The pass normalizes the timestamps,
    rebuilds torn metadata from the transcript (defaults where the transcript
    cannot answer — a zero token count reads as "unknown", an invented one would
    read as measured), and quarantines what it cannot rebuild.
  • TopologyDiff::EdgeUnknown, so an unparseable edge kind is reported as
    unknown rather than silently dropped from the diff.
  • Approval that survives inattention. A still-parked approval is re-raised
    on a backoff schedule; an attended card waits instead of expiring; launching
    an app is ungated for an attended operator.
  • SESSION_KNOBS.md, and a CLAUDE.md that stops carrying detail no single
    reference document could say — the criteria index now holds shape names, with
    triggers and full text in the FEATURE_LOCATOR appendices.

Fixed

  • 40 verified defects across five subsystems in one openclaw-reference
    hardening round, plus eight rounds of per-module logic audits (teams,
    thinker, tool_metadata, tool_output, tools, utils, vision, wizard,
    verification, workflow, resilience, looping, mcp, media, memory, harness,
    hub, logging, loop_graph, group_chat, guardrails, generation, extension,
    clarification, cli, canvas) and 22 numbered fix rounds. Highlights below; the
    reports are archived under review-results/.
  • SSRF and rebinding, closed as a class rather than a list. URL fetches in
    generation, media_send, google_meet, a2a_agents and export now
    carry the policy from config instead of each deciding for itself;
    web_fetch skips the provider path that reopened a DNS-rebinding TOCTOU;
    protocol-relative and percent-encoded bypasses in export are closed; the
    Discord token regex is anchored and joins the leak-detector assets; the PII
    email regex caps its TLD quantifier so it cannot backtrack unboundedly.
  • A failed run now reports that it failed. The exit arm computed
    HarnessError::class() and fanned all four variants into Cancelled, so a
    provider auth failure rendered on every trace surface as Info labelled
    "cancelled" — softer than a HitLimit cap and indistinguishable from the
    user pressing stop, while the session log beside it said Errored. One halt
    vocabulary now spans protocol, CLI, TUI and Panel.
  • The transcript has one order, and it is the order the rows were recorded.
    Two backends each answered "what order?" separately, and two of the disagreeing
    paths were DELETEs.
  • require_operator_tier was fail-open on an absent TurnContext, and the
    justification comment for that was unconditional — it read as if the absence
    had been considered.
  • caller_may_choose_directory was constant-true. A predicate that cannot
    return false is not a gate; five more claims of the same shape were narrowed
    at their own sites rather than at the readers.
  • A session that was stopped or ended can no longer be silently resumed, and
    the FTS delete, the source delete and the count update happen in one
    transaction instead of three.
  • Idempotency keys are namespaced by (principal, method), and the connect
    rate limit stopped locking out an entire NAT because one client behind it
    misbehaved.
  • crate::sync_primitives is now the only door. Locks, Arc and atomics in
    canvas, identity, loop_graph, extension, runtimes and the capability slots
    were reaching past it into std::sync — which is how a poisoned mutex
    becomes a panic in one subsystem and a recovered guard in the next.
  • looping refunds an iteration on Active → Paused with an unrun tick, so
    pausing no longer costs a turn that never happened.
  • A test still held the pre-lift rule after the macOS fork ban was lifted.
    elevated_subprocess_denied_by_approval_returns_error asserted that a
    subprocess-spawn capability is an escalation and gets denied — true on Linux,
    and no longer true anywhere else, because session_baseline() admits spawn
    on every non-Linux platform. The test carried its own copy of that platform
    rule, so when the owning constant changed the two silently disagreed and the
    test failed on macOS and Windows while reading like a defect in the approval
    gate. It now asks SandboxCapabilities::session_baseline().spawn_subprocess
    and asserts the real behaviour on both sides of that answer: denial where
    spawn is elevated, and — where it is not — that the gate is never consulted
    at all, so a skipped denial can never be read as a grant.
  • The macOS shell fork ban is lifted for shells, which never bought security
    ((deny process-fork) does not stop rm -rf, only compound commands) and did
    break every multi-command invocation; the per-user TMPDIR is granted.
  • Panel search settings stopped acting on names it had not resolved — an
    unresolved delete target was treated as permitted, and the default-provider
    refusal is now shown before the click rather than after it.
  • The release pipeline was gated shut, for the second time. 0dc1ff85a
    dropped the eight tracked files under interfaces/webchat/dist/ with the
    commit message "release job to take over"; no release job was taught to build
    the WASM, and the workflow still says "pre-built and committed to git — no
    WASM build here". panel-dist-check is needs: of every build, so every
    release run would have failed at the first job — and, worse, assets.rs
    embeds that directory at compile time, so any clean checkout was building
    an aleph-server with an empty Panel inside it. The same removal shut the
    pipeline for two days at 033814185 on 2026-08-13. The files are tracked
    again and the root .gitignore no longer contradicts the twenty-line comment
    in interfaces/webchat/.gitignore that explains why they must be.
  • CI: the ubuntu test job stopped fitting in 16 GB. Its own comment
    predicted this precisely — "if this dies again the sampler will show a
    decline rather than a plateau, and that is a different problem" — and that is
    what the sampler shows: 13.0 GB to 669 MiB in five minutes, then exit 143.
    16 GB of swap on the ephemeral volume, rather than another -j cap the
    earlier measurements had already shown to be inert.
  • Every continuation re-seeded its whole history. seed_history probes the
    session log for non-emptiness with get_events(id, from, to) — a seq
    range, where to is exclusive and seq allocation starts at 1, so Some(2)
    is the one-event probe. A review pass read to as a limit and lowered it to
    Some(1), which asks for seq < 1 and matches nothing on any log: the guard
    answered "empty" every time and doubled the conversation context on every
    continuation run — the exact duplication it exists to prevent, introduced by
    the edit whose commit message says it fixed it. The bound is a named constant
    now, because the literal reads like a count.
  • trace.list answered a malformed cursor with page one, and called it
    success.
    Every field of TraceListParams is #[serde(default)], so the
    only way the params object fails to deserialize is a field of the wrong type
    — and unwrap_or_default() turned that into "no cursor, default limit". A
    client paging with a stale cursor shape looped on the first page forever and
    was never told why. It is INVALID_PARAMS now. The integration test that
    should have caught this had spent the move from the single-timestamp cursor
    to the compound { last_timestamp, task_id } inside that same hole: it fed
    the compound cursor back into the i64 key, so its "no overlap between
    adjacent pages" assertion had been comparing page A against a second copy of
    page A. Both cursor shapes now have a test, and so does the refusal.
  • A Windows session directory name cannot identify its session key, which
    is now written down and asserted instead of quietly worked around.
    rebuild_metadata_from_transcript parses the directory name to recover the
    key, the agent id and the session type — correct on POSIX, where
    sanitize_key_for_dir is the identity, and wrong on Windows, where it maps
    : (the key's own separator) to _. So a repaired session came back under
    fallback labels there.
    Two inverses were written for it and both removed. The first proposed the
    all-_-to-: reading and validated it by projecting back onto the name — a
    test every candidate passes by construction, since turning some _ into
    : and mapping them back is the identity — so it returned a wrong key with
    full confidence. The second enumerated the readings and demanded a unique
    one; that is sound, and it declines on the ordinary shapes, which makes it a
    predicate false on every real input: the same thing as absent, shaped like a
    feature. The reason both fail is one fact, now pinned by
    two_real_keys_collide_on_one_windows_dir_name: agent:main:main:s1 (a main
    session at epoch 1) and agent:main_main:s1 (an agent named main_main) are
    different keys that land on the same directory name, because _ is legal
    inside an agent id. Nothing reading only the name can separate them.
    So the reader keeps the fallback labels, and the property lives beside the
    forward map where the next person to want an inverse will find it. The
    repair test asks sanitize_key_for_dir and the key parser what to expect
    instead of asserting the POSIX answer on every platform — carrying its own
    copy of the platform rule is what made it fail on Windows both times this
    code moved. The real
    fix is a reversible forward map — percent-escaping : rather than collapsing
    it — plus a rename migration for existing directories, which is a change of
    its own and not a repair to the reader.
  • Four Windows-only test failures in session_store::migration: the fixture
    joined a session key to a path, and : is illegal in a Windows filename —
    production never does that, it routes every key through
    sanitize_key_for_dir.
  • aleph-server died at startup on Windows with STATUS_STACK_OVERFLOW,
    taking every integration test that starts a server with it — 13 of them, all
    failing on the same line, which is what a shared fixture looks like when the
    thing it fixtures is broken. worker_stack_size() raises tokio's worker and
    blocking threads to 256 MB (debug) / 32 MB (release), and the comment above
    it measures exactly why that is needed: engine.execute's future alone is
    ~350 KB and a debug build allocates a full-size temporary per nested future.
    But rt.block_on(async_main(args)) runs async_main on the calling
    thread, and thread_stack_size sizes the threads tokio creates — the caller
    is not one of them. So the one future all that reasoning was about ran on
    whatever stack the OS gave main: 8 MB and growable on Linux and macOS,
    where it survived the shallow paths and hid the problem completely, and a
    fixed 1 MB from the PE header on Windows, where it did not. async_main now
    runs on a thread sized from the same worker_stack_size() the workers use —
    rather than a Windows /STACK: link argument, which would have put a second
    copy of the number in a second file with nothing keeping the two equal.
    Scope of the observation, stated precisely: what crashed is a debug build,
    the one CI spawns, whose measured appetite is far larger. Whether the release
    build shipped in the Windows app also exceeded the 1 MB main-thread stack was
    never measured, so this is not a claim that it did — the fix removes the
    question either way, since both profiles now get a stack chosen for them.
  • A worktree-isolated subagent stopped getting its own target/ dir.
    WorktreeSandbox injects CARGO_TARGET_DIR=<worktree>/target so a subagent's
    cargo builds stay out of the parent's target directory — that redirect is what
    subagent_spawner documents as the point of the whole sandbox. A late review
    pass narrowed the injection to commands whose program is cargo, on the
    reasoning that another tool reading the variable might write outside the
    worktree. Both halves are wrong. The value is an absolute path inside the
    worktree, so a tool that honours it writes within the isolation boundary, not
    outside it; and program is never cargo on any production path — every
    caller spawns an interpreter (code_exec uses language.runtime(),
    code_check and the bash tool hardcode bash) with the cargo invocation
    living inside the shell text. So the predicate was false everywhere and the
    feature was silently deleted. Restored unconditionally, with the reasoning
    written at the injection site so the next narrowing has to argue with it.
  • The Rust Doctor workflow wrote its diagnosis only to the step summary, so
    its failures — it has never been green since it landed — said nothing at all
    to anyone reading the logs. It also checked out without submodules, which
    include_dir! needs at compile time, added clippy to a different toolchain
    than the pinned one, ran without the system libraries or the externalBin
    placeholders a workspace-wide build needs, and declared no blocking level. It
    now runs; the score it reports is a starting number, not a passing one.