Repository navigation
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 pinnedvteAPI surface. The screen is held server-side and
published as diffs on a 16 ms cadence; the raw byte topic is retired.
pty.attachreplays 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/listwith 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 fromaleph projectson the CLI, from room settings in the Panel,
and fromproject_manage bind_workspace. Covered by a real-machine fixture
(qa/rooms_channel_bind). grepandfindare tools. Content search and filename discovery each
get their own verb over the shared.gitignore-aware walk, andbashstops
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 millisecondlast_active_atstamps that outranked every healthy session
in the descending sort, and tornmetadata.jsondocuments that made
list_sessionswarn 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 aCLAUDE.mdthat 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 underreview-results/. - SSRF and rebinding, closed as a class rather than a list. URL fetches in
generation,media_send,google_meet,a2a_agentsandexportnow
carry the policy from config instead of each deciding for itself;
web_fetchskips the provider path that reopened a DNS-rebinding TOCTOU;
protocol-relative and percent-encoded bypasses inexportare 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 intoCancelled, so a
provider auth failure rendered on every trace surface asInfolabelled
"cancelled" — softer than aHitLimitcap and indistinguishable from the
user pressing stop, while the session log beside it saidErrored. 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_tierwas fail-open on an absentTurnContext, and the
justification comment for that was unconditional — it read as if the absence
had been considered.caller_may_choose_directorywas 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_primitivesis now the only door. Locks,Arcand atomics in
canvas, identity, loop_graph, extension, runtimes and the capability slots
were reaching past it intostd::sync— which is how a poisoned mutex
becomes a panic in one subsystem and a recovered guard in the next.loopingrefunds an iteration onActive → Pausedwith 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_errorasserted that a
subprocess-spawn capability is an escalation and gets denied — true on Linux,
and no longer true anywhere else, becausesession_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 asksSandboxCapabilities::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 stoprm -rf, only compound commands) and did
break every multi-command invocation; the per-userTMPDIRis 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 underinterfaces/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-checkisneeds: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
analeph-serverwith an empty Panel inside it. The same removal shut the
pipeline for two days at033814185on 2026-08-13. The files are tracked
again and the root.gitignoreno longer contradicts the twenty-line comment
ininterfaces/webchat/.gitignorethat 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-jcap the
earlier measurements had already shown to be inert. - Every continuation re-seeded its whole history.
seed_historyprobes the
session log for non-emptiness withget_events(id, from, to)— a seq
range, wheretois exclusive and seq allocation starts at 1, soSome(2)
is the one-event probe. A review pass readtoas a limit and lowered it to
Some(1), which asks forseq < 1and 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.listanswered a malformed cursor with page one, and called it
success. Every field ofTraceListParamsis#[serde(default)], so the
only way the params object fails to deserialize is a field of the wrong type
— andunwrap_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 isINVALID_PARAMSnow. 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 thei64key, 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_transcriptparses the directory name to recover the
key, the agent id and the session type — correct on POSIX, where
sanitize_key_for_diris 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) andagent:main_main:s1(an agent namedmain_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 askssanitize_key_for_dirand 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-serverdied at startup on Windows withSTATUS_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.
Butrt.block_on(async_main(args))runsasync_mainon the calling
thread, andthread_stack_sizesizes 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 gavemain: 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_mainnow
runs on a thread sized from the sameworker_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.
WorktreeSandboxinjectsCARGO_TARGET_DIR=<worktree>/targetso a subagent's
cargo builds stay out of the parent's target directory — that redirect is what
subagent_spawnerdocuments as the point of the whole sandbox. A late review
pass narrowed the injection to commands whose program iscargo, 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; andprogramis nevercargoon any production path — every
caller spawns an interpreter (code_execuseslanguage.runtime(),
code_checkand thebashtool hardcodebash) 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 theexternalBin
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.