Move both submodules to current main and bring the owned delta with them.
- patches: the session title is one literal in codex_title now, so it is
anchored there and the status-card edit, which that title replaced, goes;
get_tooltip takes the keymap; the cyber-access flag names a provider in
its help; mcp_login joins the modules the library publishes.
- Cargo.toml: workspace dependencies, profiles and patches track upstream's,
as they did at the last pin, and Cargo.lock follows.
- tests: the pty spawn takes ChildFds; the feature round trip asks for an
experimental feature instead of naming one upstream has since deprecated.
Claude-Session: https://claude.ai/code/session_01SVaQNFaAjhDxthpxasGah5
The Hanzo provider asks `dev token` for its credential every ten minutes
instead of reading HANZO_USER_KEY once at startup, so a session outlives the
hour a sign-in token lives. A profile still on the old startup-only provider is
moved to it; one the user changed is left alone. `dev token` prefers an API
key, then a fresh token from the Hanzo CLI, to a sign-in token inherited from
whatever launched the agent.
The chat models api.hanzo.ai serves carry their real window and inputs, so
enso-auto and the rest no longer run on fallback metadata.
A justification without sandbox_permissions runs at the default permissions
instead of failing the shell call. Featured plugins are asked for only with a
ChatGPT sign-in, which is the only caller that endpoint answers. The profile
links to the shared credential only when it exists, and a sign-in links first
so it writes the shared one.
Co-authored-by: Hanzo Dev <[email protected]>
v0.6.107 published its GitHub release and all five platform packages,
then stopped at `cd codex-cli`: the launcher moved to npm/ on
2026-09-07 and the release never followed it. Its dependencies are pnpm
catalog entries, which npm would publish as the literal `catalog:` that
no installer resolves, so each becomes the version pnpm-workspace.yaml
gives it. npm refuses a version twice, so every publish first asks
whether that version is already there.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
The workspace says 0.7.0-dev.1 while the release computes the next patch
above what is published, so the 0.6.107 build answered dev 0.7.0-dev.1
and publish refused it. The build job now replaces the workspace
version in every manifest that pins it, in the upstream edit that gives
upstream's crates the same version, and in Cargo.lock, so --locked
holds; built that way on ra, dev --version and dev doctor both say
0.6.107.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
The platform starts one runner per job when the job is queued. With
max-parallel 1, linux-amd64 built in 22 minutes and the four targets
queued behind it never got a runner, so the release sat with one
platform's assets.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
Since 2026-09-08 release.yml sat in .hanzo/workflows, which GitHub does
not read, and the forge runs nothing for this mirrored repository: no
push to main released anything, and a draft v0.6.107 holds no assets.
It moves to .github/workflows.
The runner there is a sandbox with no root, so no step installs a
package: the build and publish jobs check for what they need (cc, make
and perl included, for the vendored OpenSSL) and name what is missing, and the Windows build takes clang-cl, lld-link and
llvm-lib from a pinned LLVM 21.1.2 release instead of apt. The five
targets build one at a time, since the hosts serve other work. The
forge mirrors this repository, tags included, so the step that pushed
the release to it, which could only fail with a GitHub token, goes.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
hanzo-kai linked hanzoai/decision's control and program crates by git.
That repository is private: the public build, CI, and every cargo install
of dev failed to fetch it. Kai is now a client of POST /v1/decisions at
api.hanzo.ai, signed with the Hanzo credential Dev already sends; nothing
of hanzoai/decision is linked, .cargo/config.toml goes, and Cargo.lock
loses 89 packages and every private source.
The programs Dev asks (tools.select, router.model, reasoning.budget,
context.select, agent.command-risk, agent.progress, agent.complete, and
the fix flow's fix.pick and fix.next) ship in crates/hanzo-kai/programs
as typed questions with their thresholds, verdict rules and calibration
pins. The gate that reads the answers is Dev's own: a noul holds at its
threshold, a choice or score is accepted at its certainty, a verdict
rule joins the policy on allow < ask < deny only when enforced, and an
answer under another calibration runs in shadow.
When Kai does not answer (refused, timed out, 401, 404, 5xx, or no
state of the decision answered) the decision fails at once and Kai is
down for 60 s: no request is made in that window, the loop does exactly
what it does without Kai, one line goes to the log and no trace line is
written. Only a decision asked after the failure brings Kai back. A
decision's states are asked in parallel; tier and budget together. The
tool shortlist is decided again when the tools on offer change, since an
MCP server that finishes starting mid-turn adds its tools.
dev flow is now dev flow fix, a fixed pipeline rather than a Decision
Program run by the private executor: retrieval, Kai's pick, Zen's diff,
the tests, Kai's next. --control rule, or Kai not answering, leaves the
tests and the budget to decide, and the report says so once. An attempt
whose patch no longer reverses ends the flow instead of retrying over it.
Tests serve the Decisions API and chat completions from wiremock: Kai
on (tools shortlisted, tier and effort hinted, verdict tightened, one
trace line per decision with its distributions), Kai answering 404,
Kai unreachable, Kai answering with a page that is not the API, tools
arriving mid-turn, and an answer under another calibration.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
hanzo-loop is the contract the loop applies: a tool shortlist per step, a
command verdict joined with the policy on allow < ask < deny, and routing
hints on the turn metadata. hanzo-kai links hanzoai/decision's control and
program at ec8207d: one controller per thread answers those three through
seven anchored upstream edits, and extension contributors feed it context,
progress and completion. kai.toml turns it on, every operation in shadow by
default. Each decision is one trace line in the shape Enso's control writes.
dev flow runs a Decision Program over the repository. The shipped fix flow
retrieves candidate files, Kai picks one, Zen writes a diff, the tests run,
and Kai says done, retry or escalate within what the tests and the budget
permit. go/dev.wasm is rebuilt because Cargo.toml, Cargo.lock and the
Makefile are among its recorded inputs.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
The core has taken the event before Step decodes its answer. Step answered a
decode that failed with ErrMalformed, which says nothing changed, and with the
actions it had read so far; the session ran on ahead of its host, and the same
turn stepped again was a redelivery that asked for nothing, so its ask was
lost. Now an answer that is not read ends the session as an ended context
does: ErrHandle, then ErrHandle for every call, and the snapshot from before
the event carries it on. A Step that answers an error answers no actions. A
stand-in core that answers with a member Action lacks, and the real core with
an answer the host refuses, each fail on the old code. The memory test drops
the host refusal it looped on, since one now ends the session.
Nothing noticed if Session.Close stopped closing the instance: the closed
session test now calls the old instance and requires it to fail.
Docs: a null member that is not an Option reads as its zero value, which serde
refuses; a session that trapped answers ErrPoison after its Core closes; and
the 64 MiB cap holds about 14 MiB of conversation and 8 MiB in one event. An
ask traps at 15 MiB of results, a Snapshot at 16, a lone prompt at 9, and a
restored session meets the same wall.
Co-authored-by: Hanzo Dev <[email protected]>
CI builds on another kind of host than the one that commits go/dev.wasm, so it
cannot compare bytes, and its go step passed whatever module was committed. A
crates/core whose cancel leaves the queue behind, committed beside the old
module, went green: the Go suite passes on both, and they answer that cancel
differently.
make wasm now writes go/dev.wasm.sum: the sha256 of every tracked file the
build reads (the three crates, Cargo.toml, Cargo.lock, rust-toolchain.toml and
the Makefile) and of the module. make wasm-check hashes the tree again and
diffs; CI runs it first. On that tree it fails on crates/core/src/lib.rs, after
make wasm it passes, and a rebuilt module committed without its sum fails on
go/dev.wasm.
Co-authored-by: Hanzo Dev <[email protected]>
The remap sat in target.wasm32-wasip1.rustflags, which cargo drops whole once
RUSTFLAGS is set: RUSTFLAGS=-Dwarnings make wasm wrote a module with five
/Users/z/.cargo paths in it. It now goes in CARGO_ENCODED_RUSTFLAGS, which
outranks every other source, and that build gives 9d28969b again. The build
names --target-dir target, so a CARGO_TARGET_DIR no longer sends it elsewhere
while an older module is copied. wasm needs prepare, as build and test do, and
rust-toolchain.toml lists wasm32-wasip1, so rustup installs it.
The Makefile said the bytes are the same wherever they are built. They are the
same on one kind of host, whatever the directories. Across hosts they are not:
cargo hashes the host into every crate that has a build script or uses a proc
macro, and this Mac, Linux arm64 and Linux x86_64 build 9d28969b, 2b40ba79 and
5648efc6, code sections and all. So CI cannot compare its build with the
committed one byte for byte. It runs the Go tests on the committed module, and
again on the one it builds from the crates beside it.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/wasm built its runtime without CloseOnContextDone, so neither a context
nor the Run bound reached the core. A Step under a cancelled context answered
as if nothing had happened, and a module whose dev_abi loops held Open past its
deadline, and would hold every garbage collection with it. The pin moves to the
hanzoai/wasm that turns it on. A call its context ends closes the instance and
answers ErrHandle, which also matches context.Canceled or
context.DeadlineExceeded; the last snapshot restores the session.
enter read gone after letting go of the session, so a Close on another
goroutine raced it. -race finds it when a gone session is stepped and closed at
once. The read now happens while the call still holds the session.
alloc handed dev_alloc's i32 the whole length, so 4 GiB and 16 bytes were lent
as 16 and writing the event poisoned the session. A length past MaxInt32 is
ErrNull before the core is asked.
Decoding took null for a variant's value, and no union at all where Action,
Done or Dispatch hold one. serde refuses both, and so does Go now. Any other
missing member reads as its zero value, and the docs say so.
Close no longer calls dev_drop and dev_release into an instance it closes on
the next line. A test repeats an event the core refuses and an answer the host
refuses: releasing the loan only on success grows memory from 1.4 MB to 30 MB,
freeing the answer only on success from 1.18 MB to 1.31 MB, and each fails.
Restore's doc says the checksum is not a signature.
The suite passes under -race here, and in linux/arm64 and linux/amd64
containers. Step, Snapshot and Restore now take about 1.3, 1.9 and 2.2 times
as long, measured on a loaded machine: the cost of the checks.
Co-authored-by: Hanzo Dev <[email protected]>
go/ is the module github.com/hanzoai/dev/go. It embeds the module `make wasm`
builds and runs it under wazero through hanzoai/wasm, so cloud drives the core
itself, with no cgo and no Rust toolchain.
Open compiles once and refuses a module whose dev_abi is not 1. New and
Restore each start an instance with 64 MiB of its own; Step and Snapshot cross
into it; Close drops the session and the instance. The out-parameter is one
twelve-byte loan per session, every input loan is released and every dev_buf
freed on error paths too, and ten thousand steps leave the memory exactly the
size it was. dev_free takes its dev_buf by value, which the wasm32 C ABI passes
as one pointer: a test pins that signature and the other eight, and that the
imports are the four WASI calls and nothing else.
dev.h's statuses are sentinel errors. A trap answers ErrPanic, then ErrPoison,
for its session alone: a 24 MiB prompt makes the allocator refuse inside serde,
and the session beside it finishes its turn. A call after Close, or after the
Core closed, answers ErrHandle; a call while another runs answers ErrBusy.
The types are crates/protocol: an enum is a struct of its variants with exactly
one set, and the encoder refuses any other count. Every type goes through the
module itself, and Go writes the core's answers back byte for byte. Breaking
the host thirteen ways, one at a time, failed a test each time.
`make wasm` copies the module to go/dev.wasm without DWARF and with the cargo
home remapped, so the bytes do not depend on where they were built: 320,088
bytes, the same from another worktree and another CARGO_HOME.
Co-authored-by: Hanzo Dev <[email protected]>
551e180a1f said the refused-allocation test stays native, and nothing kept it
there: under Miri the isize::MAX-class request is Miri's own resource
exhaustion, which ends the run instead of answering. The attribute says so, so
`cargo miri test -p dev-ffi --lib` now finishes: 20 passed, 1 ignored.
Co-authored-by: Hanzo Dev <[email protected]>
A host with no memory of its own borrows its out-parameters from dev_alloc, and
the library writes a u64 handle or a Buf into them in place. The loan was laid
out as bytes, alignment 1, so where those writes landed was up to the allocator.
Every allocator this builds against today over-aligns — wasi-libc hands back 16
in dev.wasm, malloc 16 natively — which is why nothing showed. The contract
never said so: Miri puts a one-byte loan at an odd address, and the crate's own
borrowed-buffer test is a misaligned write under it, on main as well as here.
dev_alloc and dev_release now work one layout out from len, aligned for a u64
and a Buf alike, so the promise is the library's and the pair still agrees
about what is freed. dev.h says what a loan is aligned for.
The test checks every length from 1 to 64. It passes natively with or without
the fix, because malloc is generous; under Miri it fails without it ("1 bytes
were lent at 0x1bdcdd") and passes with it, along with the 19 other ffi unit
tests Miri can run. The wasm import list is unchanged.
isize::MAX bytes have no layout once rounded up to that alignment, so the
refusal test and the C fixture ask for the longest length that still has one:
it has to be the allocator that says no for either to mean anything. Handling
that refusal the infallible way aborts both. That test stays native — Miri
cannot model an allocator that refuses, and gives up on the request instead.
Co-authored-by: Hanzo Dev <[email protected]>
A snapshot's checksum catches a store that lost or changed bytes. It says
nothing about whether the sequence the snapshot restores agrees with the host's
journal, and a snapshot older than the journal mints again ids the host has
already dispatched under — the one way to run an effect twice that the id ledger
cannot see from inside.
That warning lived only on pack(). It is now at the three doors a host author
opens to restore a session — Session::restore, dev_restore and dev.h — each
pointing back to pack() for why the check is the host's.
Co-authored-by: Hanzo Dev <[email protected]>
Cancel carried no id and stopped whatever turn was in flight when it landed.
Under at-least-once delivery that is the wrong turn: Turn 1, Cancel, Turn 2,
and the first Cancel delivered again answered Done{turn: 2, Cancelled} for a
turn nobody cancelled. Every other event was already safe to replay — a turn by
the high-water mark, a result by the outstanding ledger and its family, a timer
by the saved counter — so "for every event this protocol has" was false for
exactly this one.
Event::Cancel carries the turn id. The core acts on it only while that turn is
in flight or queued, and a turn id is accepted once, so a redelivered cancel
finds nothing under its id. In flight, the turn ends and takes the queue behind
it along, as before: a stop that started the next prompt would not be one.
Queued, the prompt goes alone, with a Save for the queue and its own Done. Any
other id changes nothing, and a cancel that arrives ahead of its turn does not
spend the id.
One event's bytes change: "Cancel" becomes {"Cancel":{"turn":1}}, spelled in
the C fixture. A host still sending the old form is refused with DEV_MALFORMED
rather than misread. ABI stays 1, as it did when Turn gained its id: nothing
has shipped under it, and a snapshot holds no event.
Three tests pin it. With the id check taken out they all fail, the first on
the redelivery's [Save, Done{turn: 2, Cancelled}].
Co-authored-by: Hanzo Dev <[email protected]>
dev_alloc built its buffer with vec!, which allocates on the path that cannot
fail: a valid layout the allocator refuses goes to handle_alloc_error, and that
aborts. An abort is not a panic, so the catch_unwind around it never ran.
dev_alloc(isize::MAX) died on SIGABRT, taking every other session in the
process with it, and the same call into dev.wasm trapped on unreachable — under
a doc and a header that both promised null.
Ask the allocator directly. Layout::from_size_align refuses a length with no
layout, alloc_zeroed answers null when there is no memory, and nothing left in
there can panic, so the catch_unwind goes. dev_release frees the buffer as the
boxed [u8] whose layout this is.
The module doc and dev.h now say how far "the process stays up" reaches: a
panic, and no further. Memory the library needs for itself still aborts when
refused — with the module's memory lent out, a dev_step on a 512 MiB input
traps — and dev_alloc is the one place a refusal comes back.
The test asks for isize::MAX bytes and the two lengths past it; against the old
body it kills the test binary with SIGABRT, and so does the C fixture asking
for PTRDIFF_MAX through dev.h. The wasm import list is unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
The C ABI builds for wasm32-wasip1 unchanged: the seven dev_* exports and the
module's memory. A Go host can then run the loop with no cgo and no Rust
toolchain in its own build, and a panic is a trap the host survives rather
than something that has to be caught before it crosses a C boundary.
One thing was missing. Compiled to wasm the library's memory is its own, so a
host has no way to produce an input pointer or an out-parameter — every one
has to name memory inside the module. dev_alloc lends it zeroed bytes there
and dev_release takes them back. A boxed slice rather than a Vec, so the
allocation is exactly the length asked for and there is no capacity for a
host to remember. A native host shares an address space and calls neither.
`make wasm` builds the module now; it used to build protocol and core alone,
on the premise that ffi was native by definition.
What the module IMPORTS is the proof that the core touches nothing:
environ_get, environ_sizes_get, fd_write and proc_exit, and no file, socket,
clock or random source.
Measured from a wazero host: compile 85ms once, instantiate 46us, a step
86us, a snapshot 21us, about 1 MiB a session. A first turn answers
[{"id":1,"op":{"Model":{...}}}] — the model ask, carrying its id.
Co-authored-by: Hanzo Dev <[email protected]>
inside() is lexical, which is the whole of what a crate with no filesystem
can say: a symlink inside the workspace that points out of it resolves
outside it, and only the host holds the tree to see that. And name what a
DEV_MALFORMED on the way out would mean, since the arm is dead for types
made of integers, strings, options and enums.
Co-authored-by: Hanzo Dev <[email protected]>
observed() and answered() removed the outstanding entry before comparing the
family it was dispatched to, so a result from the wrong family — refused,
answering with nothing — deleted the entry the real result needed: the turn
wedged forever, or resumed early and asked the model with an effect still in
flight. Compare, then remove.
Message becomes User, Agent{text, calls} and Tool{id, text, failed}, so the
transcript keeps the call each result answers instead of throwing it away; a
provider that needs the assistant tool call ahead of the result can be driven
from this state. The prelude moves to Ask, where a system prompt belongs.
A Turn carries the host's id, and the core refuses one at or below the highest
it has accepted, so a redelivered prompt runs once. A Timer asks for a Save only
when the sequence has moved. Done names the turn that asked, a prompt queued
mid-turn answers with a Save, and a cancel says what became of each — so N
prompts produce N Dones and nothing is accepted in silence.
A path in a call is workspace-relative; the core refuses one that is absolute or
climbs out rather than leaving every host to confine /etc/passwd itself. Patch
names the one file it edits. What may run and what may be fetched stays policy,
and policy is /v1/dev's.
A snapshot carries its ABI and a checksum of its body, so a truncated, foreign
or edited one is refused rather than restoring a session that re-mints ids the
host has already dispatched under. It is not a signature: there is no key here
and no identity.
Slab::put swallowed a failed lookup, so a dev_drop landing inside a step
answered DEV_OK and dropped the session it had just stepped. put hands the value
back, enter answers DEV_HANDLE and lends nothing out, and remove keeps a slot
whose value is on loan out of the free list until the call lets go. encode
returns a Result instead of panicking inside a library, and the doc no longer
claims dev_abi runs inside catch_unwind.
43 tests, and each fix is pinned: putting the defect back fails the test that
names it, 14 for 14.
Co-authored-by: Hanzo Dev <[email protected]>
zap-proto is published and is a runtime for generated code; the generator
needs the zap schema compiler, which this repo does not build with.
Co-authored-by: Hanzo Dev <[email protected]>
protocol carries the events and the ten actions, and every action carries
the id its dispatch and completion are recorded under. encode and decode
are the only two functions that know the byte format, so ZAP takes over
from serde_json without moving a type or a symbol.
core is a Session with step, snapshot and restore, and no effects at all:
no filesystem, no process, no socket, no runtime. codex-core is not
separable as it stands — tokio, an HTTP client, a rollout store, a pty —
so the model-request and tool-result loop is driven here, and the module
doc names what stays coupled upstream. An observation is accepted only
while its id is outstanding, so a replayed result runs nothing twice.
ffi is the C ABI: seven symbols, generational handles so a stale one is
refused rather than resolving to the next session, catch_unwind on every
entry point so a panic poisons one session instead of aborting the
process, and dev.h written beside it. A C program exercises the archive.
protocol and core build for wasm32-wasip1; make wasm is the check.
Co-authored-by: Hanzo Dev <[email protected]>
typescript 7.0.2 is the native compiler and exports no JavaScript API, so
ts-jest, tsup's dts build and typescript-eslint cannot read it. It comes in
aliased as tsgo and does the typechecking through new typecheck scripts;
typescript stays on the 6 line for the tools.
shell-tool-mcp asked for moduleResolution "Node", which TS7 removed, so it
moves to node16 alongside its module; the node and jest globals it uncovers
are now declared rather than swept in. 30 diagnostics before, 29 after, the
same on both compilers.
Co-authored-by: Hanzo Dev <[email protected]>
Release and bump commits were authored by the automation address, so the work never reached the owner's contribution graph.
Co-authored-by: Hanzo Dev <[email protected]>
Release / darwin-amd64 (push) Blocked by required conditions
Release / darwin-arm64 (push) Blocked by required conditions
Release / linux-amd64 (push) Blocked by required conditions
Release / linux-arm64 (push) Blocked by required conditions
Release / windows-amd64 (push) Blocked by required conditions
Release / publish (push) Blocked by required conditions
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.
One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.
Co-authored-by: Hanzo Dev <[email protected]>
The first cut died at `curl` exit 22 with nothing about the cause: `set -e`
took the run down before the message meant to explain it, and the step also
reached for jq in the one job that provisions no toolchain.
Now each failure names itself — an empty client id, the HTTP code KMS answered
(body redacted), a login with no accessToken, or no readable token on any path —
and the JSON is read with sed, which is present before anything is installed.
Co-authored-by: Hanzo Dev <[email protected]>
The forge org carries KMS_CLIENT_ID and KMS_CLIENT_SECRET and nothing else, so
secrets.GH_PAT expanded to empty and every call left as
Authorization: Bearer
GitHub answered 401, the draft release was never opened, and all five platform
jobs and publish skipped behind it. Nothing has shipped since v0.6.106 on
September 2 — a release train stopped by a header with nothing after the space.
The publish job already reads NPM_TOKEN from KMS; the GitHub token now arrives
by the same road, in the three jobs that need it, and a token KMS will not hand
over fails the run loudly instead of expanding to nothing.
Co-authored-by: Hanzo Dev <[email protected]>
logout cleared the saved Hanzo key whenever the file existed, so signing out
of ChatGPT deleted a Hanzo key the user pasted weeks ago. The two credentials
are separate accounts; destroying the one nobody asked about is a surprise
nothing undoes. The gate caught it:
assertion failed: home.path().join("hanzo-api-key").exists()
logout_removes_only_the_selected_providers_saved_key
The active provider now decides which credential goes. The Hanzo side still
runs FIRST, because run_logout exits the process and anything sequenced after
it is unreachable — the defect that once printed success while leaving the
credential readable.
Co-authored-by: Hanzo Dev <[email protected]>