Skip to content

chore(release): v0.4.0 — merge develop into main - #149

Merged
ajianaz merged 38 commits into
mainfrom
chore/release-v0.4.0-main-sync
Oct 1, 2026
Merged

ajianaz merged 38 commits into
mainfrom
chore/release-v0.4.0-main-sync

Conversation

@ajianaz

@ajianaz ajianaz commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

What (description of the change)

v0.4.0 release sync: merge develop (32874e9, workspace 0.4.0 — dependabot #141/#142 + release-content PR #148) into main as a merge commit (GitFlow release pattern; the v0.4.0 tag will be cut on this merge commit after CI green + owner tag approval, per #134).

Note for reviewers: the tree diff vs develop is intentionally almost empty — exactly one file, CHANGELOG.md, +24/−0. During the v0.3.0 release flow, three entries (#68 hardening, #70 AllowlistApprover constructors, #73/#76 eval harness) landed on main but never flowed back to develop; develop's [0.3.0] section has been silently missing them since. This merge preserves main's complete section (no-loss merge, auto-resolved) — so the PR carries those 24 restored lines plus nothing else. Verified: git diff origin/develop HEAD --stat = CHANGELOG.md only.

Why (the problem you're solving)

Issue #134 — cut v0.4.0. main is the release branch; develop is the source of truth going forward. The 24-line CHANGELOG restoration also fixes the record-keeping drift so the next release sync doesn't keep re-discovering it.

Testing (how you verified this works)

  • Merge tree = develop ∪ {restored CHANGELOG lines}: git diff origin/develop HEAD --stat shows exactly one file.
  • No main content lost: main's [0.3.0] section intact in HEAD (git show HEAD:CHANGELOG.md).
  • QA sandbox E2E on the 0.4.0 release binary (all PASS): live tole run mission (write_file+read_file with --allow, cora MCP auto-preset 18 tools), tole serve REST (401 without token on /sessions, 200 with), tole serve --transport mcp (rmcp 3.4.1 handshake, tools/list returns the 12-tool multi-session surface), tole acp clean EOF exit.
  • CI runs the full required gate set on this merge head.

ajianaz and others added 30 commits September 18, 2026 08:35
- README rewritten for v0.3.0 reality: status line (chat-first harness
  complete, E9 in progress), ecosystem position (cora = code intel,
  uteke = memory, MCP for everything else), quickstart, full tool table
  with risk tiers, approval-gate summary. Fixes the stale 'Phase 2'
  status (second occurrence of the #33 regression).
- AGENTS.md added (did not exist): agent-facing ground rules — branch/PR
  flow, mandatory cora review gate with exit-code actions, fmt/clippy/
  test verification, design rules (platform-agnostic core, append-only
  JSONL, approver gates, MCP never-trusted), live-mission binary
  hygiene, docs-sync rule, uteke repo-tole recording convention.
- docs/epics.md synced to code truth: Track A (A1 --allow, A2 git tool)
  and Track B (B1-B4) marked DONE — both were implemented in v0.3.0 but
  the doc still listed them as planned. Track C relabeled in progress.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Embedder profile (--no-default-features: no subprocess tools, no MCP)
is now compile-checked on aarch64-apple-ios (macos-14) and
aarch64-linux-android (ubuntu runner + preinstalled NDK). Check-only —
no link, no device. Android needs the NDK clang for bundled sqlite, so
the job points the cc crate env vars at the newest runner NDK.

Both invocations verified locally before landing: the iOS check on this
macOS host, the android check with the same CC/AR env pattern against
NDK r28.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
The Branch Naming check requires feat/* (not feature/*) plus the other
allowed prefixes — AGENTS.md documented the ecosystem convention, not
this repo's CI. Discovered by the check failing on
feature/cora-mcp-autopreset.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix: mcp-gated code compiles clean (never reached by default-feature CI)

The mcp feature was opt-in, so 'cargo check/clippy/test --workspace'
with default features never compiled the mcp paths — and they had
rotted: a moved-value borrow error in register_server_tools, an unused
PathBuf import, two deprecated rmcp::model::ClientInfo uses (now
ClientConfig per rmcp 3.3), and mcp_tests.rs calling risk() without the
Tool trait in scope. All fixed; no behavior change.

* feat: cora MCP auto-preset — zero-config code-intel attach

When the cora binary is on PATH, tole now attaches the local 'cora mcp'
server automatically: the full code-intel surface (brain search, callers,
impact, affected tests, dead-code, review — 18 tools, registry names
mcp_cora_*) registers with no manual flag, matching the ecosystem goal
that sibling tools are usable out of the box.

- tole-cli default features now include the mcp client
  (default = ["shell-tools", "mcp"]); embedder profiles are unaffected
  (tole-core defaults do not change).
- --no-auto-mcp global flag skips the presets per run; an explicit
  --mcp-server cora=... spec replaces the preset for that name (explicit
  wins over auto in the pure, unit-tested merge_mcp_specs).
- cora_search now follows the same startup-probing contract as the uteke
  tools: a missing cora binary degrades to a one-line warning instead of
  a phantom tool, and the native single-tool fallback is skipped when
  the cora MCP surface is attached (no duplicate search tools).
- Trust model untouched: MCP tools stay Risk::Write with the approval
  gate on every call; server metadata is never trusted.

Live E2E (release binary): auto-preset registers 18 tools in ~23ms;
--no-auto-mcp attaches nothing; explicit --mcp-server cora=... yields
exactly one registration (no duplicate).

* fix: cora_search fallback keyed on MCP registration outcome

CodeCora code-scanning finding: the native fallback decision used
config presence (mcp_servers contains 'cora'), but registration happens
later — a cora binary whose MCP server fails (old version, bad
handshake, timeout) left the user with zero code-intel tools while the
native cora_search was already skipped.

Now the MCP loop records the cora server's registration count and the
native fallback decision runs AFTER it: no MCP cora tools → register
native cora_search (when the binary exists); MCP tools present → skip
the fallback. Live-verified both paths: a fake failing cora mcp
degrades to the native tool with a one-line server warning; the real
cora mcp registers 18 tools and the fallback stays dormant.

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Cross-session memory as owner-configured harness behavior, not model
tools: the model can neither trigger nor suppress the loop.

- Pre-turn recall (--memory uteke / TOLE_MEMORY): on the first turn of
  a fresh session, prompt-relevant memories are recalled from the
  owner's uteke store and injected into the user message inside a
  clearly marked fenced block; the durable log stores exactly what the
  provider saw. Truncation happens at the single format choke point.
- Post-session remember: when a session settles with a final answer, a
  compact summary (--type context, tags tole,session) lands in the
  namespace. The summary stores the PRE-injection prompt — storing the
  injected block would echo recalled memory back into the store
  (amplification, caught in live E2E round 1 and fixed).
- Namespace follows the ecosystem repo-<dir> convention
  (TOLE_MEMORY_NAMESPACE overrides, argv-safe sanitization); every
  failure degrades to stderr and the turn proceeds without memory.
- Host knobs (workspace / mcp_server / memory) bundled into a shared
  HostConfig so command signatures stop growing per feature; the
  clippy too_many_arguments allow is gone.
- memory.rs: fake-binary e2e tests pin the exact CLI argv contract for
  recall and remember; sanitization and fence formatting unit-tested.

Live E2E (release binary, real uteke store via scratch namespace +
bifrost provider): injected recall answered a fact only present in
memory (BUAH NAGA / Tuesday); summary verified in the store, no echo;
test memories cleaned up afterwards.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…d rewrite, CLA files (#91)

Docs pass aligned with the uteke workflow-standard rooms
(codecora-workflow-standard + codecoradev-conventions):

- README: badges, install from crates.io (tole-cli IS published — the
  'build from source only' claim was stale), CLI command table, env-var
  reference table, sessions-location note, ecosystem repo links,
  workspace/evals layout, documentation index with CLA links.
- CONTRIBUTING: rewritten to the ecosystem standard — branch-name
  prefixes, issue-first + templates, PR body sections (What/Why/
  Changes/Testing), merge-gate checklist (real Cora verdict required,
  not just a green check), Cora Review 10-min split rule, CLA section,
  crates.io claim corrected.
- AGENTS.md: restructured to the adopted 6-section Gatehouse standard
  (repo layout, branch & merge discipline, pre-commit checks, tooling,
  cross-session continuity, infra facts). New tooling mandate: cora
  index/brain/callers/impact/affected/query before and during coding
  (brain over blind grep), review/scan after; uteke recall/remember for
  cross-session continuity (namespace repo-tole, standards rooms are
  the source of truth).
- CLA_INDIVIDUAL.md + CLA_CORPORATE.md added (org-generic, mirrored
  from codecoradev/cora-code per the every-repo-ships-CLA standard).
- CHANGELOG: deduplicated the double '### Added' heading under
  Unreleased (artifact of merged PRs).
- docs/architecture.md: 'Cross-build CI deferred to Phase 3' stale
  claims updated (landed in #82).

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…#92)

Full cora scan (54 files, 2026-09-18) reported 10 MAJOR findings; all
10 validated against the code, 9 real. This PR fixes them:

- openai: derived Debug printed api_key in cleartext via {:?} — manual
  redacting impl (safe_description stays the explicit form) + test.
- uteke: recall query could inject CLI flags (leading-dash guard, same
  defense as the room check) + test; room-link spawn skipped
  scrub_env_for_child (ENV-1) — scrubbed.
- jobs: job_poll (ReadOnly) truncated/rewrote the job log on poll —
  the write-back is gone; bounded tail read only (risk-tier invariant
  outweighs the disk-space convenience; JOB-2 OOM protection stays) +
  updated test asserts non-mutation.
- subprocess: capture capped at 32 MiB per stream with a marked
  truncation suffix (unbounded read_to_end allowed memory exhaustion);
  drain no longer joins unconditionally — a grandchild holding the pipe
  gets a 2s grace, then the agent proceeds (leaked-but-doomed reader
  beats an unbounded hang; unterminated output dropped, documented);
  run_with_timeout_stdin's writer join got the same grace (caught by
  cora review on this very PR).
- subprocess argv scan: sh -c payloads now get full scan() recursion
  AND keep the conservative whole-payload match — path-qualified
  programs (sh -c "/bin/rm -rf /"), nested shells, and quoted tokens
  ('rm -rf /') are refused; compound statements still caught. +tests.
- gh: pr_create advertised 'head' but argv silently dropped it — PRs
  could open from the wrong branch; now passed and validated like base
  + tests.
- docs: architecture.md Destructive row contradicted the
  never-allowlistable invariant — corrected.

The 10th MAJOR (mcp single-oversized-block cap) is fixed too, via an
incremental pure cap_text_blocks helper + tests. Deferred MINOR/INFO
findings are listed in the PR body for follow-up.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
)

Deferred MINOR/INFO findings from the 2026-09-18 full cora scan, fixed
directly (per maintainer call: no tracking issue):

Provider wire:
- openai: scrub() is a no-op for an empty secret (replace('') mangled
  the whole message); non-string tool_call arguments are serialized
  instead of silently replaced with '{}'; one ureq agent per provider
  (connection reuse) instead of a fresh agent per request.

CLI host:
- NEW root-cause fix found by live E2E of this sweep: write_file was
  the ONLY registered tool without a spec() override — providers got a
  property-less schema and GLM legally answered arguments:{} (missing
  'path' every call; loop guard tripped). Spec declares path+content;
  regression test pins the schema.
- failed startups no longer leave a stray empty session file (registry/
  provider are built before the session is created, run + chat).
- resume <id> "<prompt>" stores the memory summary like run does.
- chat says explicitly when a typed message was dropped after exhausted
  mid-flight retries (the old copy implied it was recorded).
- session ids use strip_suffix (x.jsonl.jsonl no longer yields an
  unusable id); binary_available honors the executable bit (unix) and
  .exe (windows).
- tole-cli is now lib+bin: approval_flow integration tests drive the
  REAL jailed WriteFileTool and InteractiveApprover instead of drifting
  re-implementations (the scan's parity finding, fixed structurally).

Core tools:
- edit_file's approval line shows the actual old→new change (bounded
  previews), not just the file name.
- edit_file temp files are unique per attempt (pid+nanos) and legacy
  pid-only stale temps are swept up front — self-heal can no longer
  race a concurrent same-pid writer.
- delete_file on a symlinked path removes the LINK, not the referent
  the jail canonicalized to.
- job_start kills the spawned job when the pid file cannot be written
  (no untracked orphan reporting failure).
- mcp: tool-request descriptions truncate by streaming (no full
  Vec<char> of the payload); transport-error class shares one constant.

Docs/evals:
- architecture.md principle 4 + cross-build rows updated (CI landed).
- threat-model ENV-1 row now lists the real scrub set (_TOKEN,
  PASSWORD included); JOB-2 row matches the post-triage read-only poll.
- evals Tier 2 runner: Python 3.9-compatible annotations, mkdtemp
  session dirs cleaned, portable ISO timestamp, baseline diff keeps
  improvements out of regressions and notes zero baselines.
- tests: jsonl tmpdir clears stale state, turn_loop asserts the abort
  outcome instead of discarding it, cora_search timeout tests unix-
  gated, evals_tier1 takes &Path, approver dead counter removed.

Dismissed with evidence (see PR body): gh read ops raw stdout, session
listing full replay cost, Ctrl-C summary skip, PID-recycling liveness,
PRD/epics historical risk-tier wording, architecture open-question
rows.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
)

Approved 2026-09-18: three hosts on top of tole-core, in build order —
D1 tole mcp (MCP server over stdio, #94), D2 tole acp (Agent Client
Protocol host for editors, #95), D3 tole-serve (HTTP daemon with auth,
#96). Execution order extended accordingly.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…98)

* feat: tole mcp — serve tole's tools as an MCP server (D1, issue #94)

tole as an MCP server over stdio (rmcp ServerHandler over the ordinary
ToolRegistry): the registry's hardened tools — jailed file ops,
argv-validated git, detached jobs, memory loop, cora/uteke integrations
— become callable by any MCP client, instead of being locked inside the
CLI process.

- Approval policy in server context: no stdin human (stdin IS the
  protocol), so the interactive approver is replaced by explicit
  pre-authorization — ReadOnly always callable; Write requires --allow
  patterns (denied calls settle as tool errors with an actionable
  message); Destructive structurally absent (registration behind a
  non-interactive approver is refused — the three-layer invariant holds
  unchanged).
- tole mcp subcommand: server-mode registry (same hardened tools, no
  MCP-client nesting, no interactive prompts) + serve_stdio.
- Tests: in-process duplex end-to-end over the real rmcp server+client
  pair — listing (schemas mirror spec()), write denial without --allow,
  write execution with --allow, unknown tool, Destructive absence.
- Live E2E: initialize/tools/list (11 tools, Destructive absent),
  read_file round-trip, write without --allow denied with an actionable
  message, write with --allow executes.
- Docs: README ecosystem bullet, CHANGELOG Added, epics D1 marked done
  (+ fixed escaped backticks the Track D doc shipped with).

* style: drop unused Arc import in mcp_server tests

* fix: Destructive tools uncallable in server mode regardless of approver

CodeCora scan finding on the MCP server: execute_checked grouped
Destructive with Write behind the approver decision — an embedder
passing a permissive registry (interactive+allow) got a Destructive
tool hidden from tools/list yet callable via tools/call, contradicting
the module's documented invariant.

Now the guard is structural: Destructive refuses outright in
execute_checked before any approver is consulted, with a regression
test that registers a Destructive tool behind a permissive interactive
approver and asserts it is both hidden and uncallable.

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Dogfood finding (2026-09-18): GhTool's --repo was hardcoded to
codecoradev/tole at registration, so running tole in ANY other project
made gh issue/pr ops act on the wrong repository.

The CLI now asks the checkout itself (git config remote.origin.url) and
derives owner/name — https/ssh/URL-suffix forms, strict charset,
leading-dash refused. Falls back to codecoradev/tole when there is no
GitHub origin. Applied to both registry builders (interactive CLI and
MCP server mode); URL parser + real-checkout detection covered by
tests.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
#83) (#101)

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…cho (#102) (#104)

* docs: CLI copy sweep — de-internalize help, truncate approval input echo (#102)

* fix: keep head and tail in the approval input preview (cora MAJOR)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
#105)

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Adopt the Dream-RSI replay-evaluation pattern (arXiv:2609.14858) inside
the existing eval harness. Tier 2 runs can now archive a redacted copy
of each mission trace (evals/traces/<model>/<mission>/, secret-shaped
tokens dropped, host paths scrubbed) via run.py --archive, and a new
operator-built scorer bin (tole-replay) walks those traces under a
candidate prompt at zero tool executions — recorded outcomes settle
every tool call. The incumbent default prompt is always evaluated in
the same run (extracted from the CLI source, never a drifting copy) and
a revision only ships when strictly better on the corpus (monotone
selection). Divergent/errored walks score zero: replay is exact only
inside the recorded world. results.json gains the model field and the
runner accepts TOLE_EVAL_BINARY. Live-validated end-to-end on glm-5.1:
3/3 missions archived, replay scored, a degraded no-tools candidate
rejected (0.783 vs 0.842 incumbent avg).

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
--plan-mode filters the session registry to Risk::ReadOnly tools via
ToolRegistry::retain_read_only(): write/delete/run tools are ABSENT
from the wire (the model cannot even see them), not merely approval-
gated. The default system prompt gains a matching read-only sentence
through default_prompt_for(); the incumbent non-plan text is byte-
identical to before, so the replay scorer's grep-based incumbent
extraction is unaffected. Explicit --system / TOLE_SYSTEM_PROMPT
still win (B2 precedence); the pinned header re-applies on resume.

Live E2E (glm-5.1): plan-mode write mission -> zero write intents in
the durable log (read_file only), no file created, the answer is a
plan; identical mission without the flag -> write_file intent, file
created. Registry unit tests cover the filter and its invariants.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat: dynamic context sections in the default system prompt (#111)

Fresh default-path sessions pin a one-line Context section (working
directory + today's UTC date, civil-from-days) after the incumbent
prompt — removes a whole class of clock/cwd probing turns at zero
runtime cost. Extracted civil_from_days() from fmt_mtime() (shared
Hinnant algorithm); explicit --system / TOLE_SYSTEM_PROMPT still win;
the assembled prompt is header-pinned so the per-session append-only
wire body (KV-cache prefix property) is untouched.

Live E2E: with the section, a no-tool question about date + cwd is
answered correctly in one line; the section is durable in the session
header. Unit tests: prefix preservation, date shape, known-date table.
Compose note: rebases cleanly under plan mode's default_prompt_for()
as build_default_prompt(plan_mode).

* fix: drop duplicated #[test] attribute (clippy -D warnings)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Cora-caught MAJOR: the post-hook snapshot sat outside the ReadOnly
guard, so with hooks wired every read_file call spawned the post-hook
processes — contradicting the documented zero-overhead contract for
ReadOnly tools. Gate the snapshot on risk and keep both notify calls
on the snapshot.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…n families (#115)

Full cora scan on develop @116461c found 3 MAJORs; all fixed:
- #23 storage: a header line persisted WITHOUT its trailing newline
  (crash mid-header-write) parsed fine and open() handed back a live
  append writer — the next append glued a body line onto the
  newline-less header and bricked the session. open() now fails
  explicitly with a Corrupt error naming the recovery path
  (create_with()); regression test drives the real fixture (header
  bytes minus newline) through fail-open + recreate.
- #30 evals/replay: the incumbent-prompt extractor skipped escape
  sequences instead of decoding them (\n, \t, \", \\ vanished;
  \<newline> continuations mishandled), so the replay corpus would
  drift from the runtime prompt the moment the default prompt gains
  any escape. Decodes now follow Rust string-literal semantics;
  verified live: incumbent still scores 1.0 on the trace corpus.
- #31 evals/tier2 redaction: extends the secret families to GitHub
  ghs_/ghu_/ghr_ + GitLab glpat- + Slack xoxa- + AWS AKIA ids, with
  the stub helper kept in sync; verified all new families stub out
  with zero leakage and prose untouched.
Also: CODE_OF_CONDUCT.md still said 'Uteke' — de-vestigialized to tole.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
… (#122)

A named pipe inside the workspace passed the directory check,
reported len() == 0 (so the size cap never fired), and open() BLOCKED
until a writer appeared — a hang inside a ReadOnly tool that freezes
the whole turn loop. execute() now requires meta.is_file() and
refuses anything else with a clean error. Regression test creates a
real FIFO (mkfifo, skipped when unavailable) and asserts the refusal
— if the guard ever regresses, the test itself hangs, which is the
point.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…123)

* fix(storage): poison on failed append + O(new) commit id validation (#116)

- append_line() failure mid-write (classic ENOSPC partial write) now
  POISONS the storage: commit() refuses every later append with a
  fatal reopen-directed error. Previously the storage stayed usable
  and a retry glued its line onto the newline-less fragment left at
  EOF, bricking the session on every subsequent open — the same
  failure family as the torn-header fix in 48141f2.
- commit() id validation is O(new entries): parent checks use
  by_id.contains_key() and only the current commit's ids go into a
  local set — cloning the whole by_id index per commit made long
  sessions O(N^2).
- Tests: /dev/full injection drives the poison contract end-to-end
  (fail -> gate -> untouched memory -> reopen recovery; chmod-based
  injection rejected by cora as ineffective against an already-open
  fd); intra-commit parent + cross-commit duplicate assertions.

* fix(test): /dev/full is Linux-only — skip gracefully on other unix (CodeCora CI)

* test: assert compact recovery arc end-to-end in the poison test

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…119) (#124)

- openai complete(): a non-2xx response is now surfaced as
  'http status: N — <scrubbed body snippet>' instead of being mangled
  by read_json into a misleading 'malformed response'. The phrasing
  is load-bearing: the turn loop's transient-retry classification
  (#66) matches 'http status:' so 429/5xx keep auto-retrying.
- git add lexical jail: reject Windows drive-letter absolute paths
  (C:\..., C:/...) that slipped past the '/'/'\\' checks (cora
  full-scan #30); colon at position 1 only, 'weird:name.txt' stays
  legal. Unit tests cover all four absolute shapes + relative pass.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…120) (#125)

Three cora full-scan MAJORs on the command surfaces:
- run_command describe() re-quotes tokens containing whitespace,
  quotes, or shell metacharacters (embedded quotes widen to '\'' ):
  approval prompts now preserve token boundaries — 'printf a b' (one
  arg) renders differently from 'printf a b' (two args).
- benign_commands test EXECUTES 'rm -rf ./build' against the SHARED
  temp dir; now each run gets an isolated scratch jail with its own
  build/ dir (peer tests' files can no longer be deleted).
- job_start ids gain a process-wide AtomicU64 counter segment
  (j-<ms>-<pid>-<n>): two starts in the same millisecond can no
  longer collide on the same id.
Unit tests: ambiguous-argv rendering matrix, id uniqueness covered by
the counter; full suites + clippy -D warnings green.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…126)

* fix(mcp+chat): spawn_blocking tool execution; never drop typed input (#121)

- mcp_server call_tool runs execute_checked (subprocess waits of up
  to 420s) via tokio::task::spawn_blocking so one slow tool call
  cannot pin the async runtime's workers; RegistryServer is now
  Arc-Clone to move a handle into the blocking task. Existing
  end-to-end MCP tests exercise the new path (mcp feature).
- chat REPL resolve loop: UnknownTool / BudgetExhausted /
  LoopDetected / Storage outcomes and resolve Err now flag
  dropped_message (the typed line never reached the durable log) and
  the Err path prints the same NOT-recorded note before bailing —
  closing the silent-input-drop gap cora full-scan #7 flagged for
  non-retryable outcomes (B1 contract: user input is never silently
  dropped).

* style: cargo fmt (mcp_server chain wrapping)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Every [[bin]] target builds unconditionally, so the eval scorer
shipped in plain builds and cargo install tole-cli — contradicting
the documented operator-only contract (cora full-scan #12, my error
from #108). required-features = ["replay"] with a non-default
replay = [] feature: default builds/installations never produce it,
eval sessions opt in via --features replay. Build commands in
evals/README.md + run.py docstring updated. Verified: default build
produces no tole-replay, --features replay builds it, package list
excludes replay.rs.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
… scrub, cora_search flag guard, uteke spec truth (#128)

- tole mcp: --plan-mode now filters the SERVED registry to read-only
  tools and --on-pretool/--on-posttool error out loudly instead of
  being silently ignored (scan-3 #9 — global args parsed but
  unhandled by the mcp subcommand).
- chat REPL: dropped_message is scoped per message; previously one
  drop warned on every later message forever (scan-3 #8).
- hooks: hook subprocesses scrub secret-shaped env like every other
  child spawn (scan-3 #23).
- cora_search: leading-dash query guard, mirroring uteke_recall
  (scan-3 #32).
- uteke_document spec no longer advertises title/tags that execute()
  silently drops (scan-3 #33) — the spec now matches reality.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
… guard, uteke spec truth, mcp plan-mode, chat flag scope (#129)

- hooks: hook subprocesses scrub secret-shaped env like every other
  child spawn (scan-3 #23).
- cora_search: leading-dash query guard, mirroring uteke_recall
  (scan-3 #32).
- uteke_document spec no longer advertises title/tags that execute()
  silently drops (scan-3 #33).
- tole mcp honors --plan-mode (read-only served registry) and loudly
  rejects --on-pretool/--on-posttool (scan-3 #9).
- chat REPL dropped_message scoped per message (scan-3 #8).

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat: tole acp — Agent Client Protocol host (D2, issue #95)

tole as an ACP agent over stdio: editors and ACP-capable clients (Zed
et al.) drive durable tole sessions via line-delimited JSON-RPC, with
no new dependencies.

- initialize / session/new / session/load / session/prompt: ACP
  sessions map to the durable JSONL store (session jail = the client's
  cwd; load replays the existing log).
- session/prompt runs one full tole turn and delivers the final answer
  as an agent_message_chunk update before responding (v1: no intra-turn
  streaming — the synchronous turn loop is untouched).
- Approval bridge: Write/Destructive tool calls surface as
  session/request_permission requests to the EDITOR — the human in the
  client is the approver, which is why Destructive tools CAN be
  registered here with genuine per-call consent (unlike MCP server
  mode), without weakening anything.
- Prompt accepts the ACP content-block array form (and plain strings).
- run_command/git/gh-detect operate on the SESSION cwd, not the process
  cwd (CodeCora review finding on this PR).
- Provider config is only required when a prompt actually runs.
- CI-safe integration test spawns the real binary: initialize, session
  lifecycle, unknown-method error; live E2E covered permission flow,
  streaming chunk, and stop reasons.

Closes #95 (Phase D2).

* fix: ACP session map survives across prompts; id traversal + poison hardening

CodeCora review on the ACP PR caught a real architectural bug: the
prompt arm wrapped a FRESH empty map per prompt (share_sessions used
mem::replace), so every session vanished after its first turn and
session/load could open a divergent second handle mid-turn.

- The session map now lives for the whole server lifetime (Arc clone
  per prompt thread); the map lock is held for the duration of a turn,
  which serializes a busy session instead of allowing divergent
  appends.
- Poisoning-tolerant locking: one panicking turn no longer bricks the
  ACP server.
- sessionId validation (charset, length, no separators/parent refs)
  blocks path traversal via session/load before ids touch the
  filesystem; regression tests cover multi-session persistence and
  traversal rejection.

* fix: ACP reader loop no longer deadlocks behind a running turn

CodeCora review deadlock finding: run_prompt held the session-map lock
for the WHOLE turn — including the up-to-600s permission wait — and the
reader loop's session/new/load arm takes the same lock. A client
opening a session while a permission request was pending froze protocol
routing until the timeout denied the tool.

- SessionState storage/registry/first_prompt_done are now Arc-wrapped;
  the map lock is held only to take handles and reject a busy session;
  the running turn locks its OWN storage mutex.
- Busy flag with a panic-safe Drop guard: concurrent turns on one
  session are refused ("session is busy"), and a panicking turn
  un-busies via Drop.
- Live-verified: three consecutive prompts in one session all
  end_turn (previously the map swap broke even single-turn
  persistence); handshake integration tests stay green.

* chore: retrigger CI for the D2 PR (previous push predated a stale CI state)

* fix: CodeCora round-2 on the ACP host — plan-mode filter, busy load, error routing

- plan_mode now REALLY filters: open_session registered every tool and
  the early return still handed back the full registry — under --yes
  that pre-authorized writes in a supposedly read-only session. Write/
  Destructive registrations are now skipped entirely when plan_mode is
  active (read_file/cora_search/uteke_recall/job_poll remain).
- session/load refuses a busy session: the old path opened a second
  JsonlStorage handle on the same JSONL while a turn was running,
  allowing divergent concurrent appends (the orphaned busy flag no
  longer guarded anything after the state was replaced).
- client ERROR replies to session/request_permission are routed like
  results — an errored/cancelled permission now fails closed
  immediately instead of hanging the turn for the full 600s timeout.
- also: duplicated too_many_arguments attribute removed (CI clippy).

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…#131)

tole as a long-running HTTP daemon: REST endpoints for the session
host — create/list sessions, run turns, poll status. Remote clients
drive durable tole sessions without SSH-ing into the box.

- session_host.rs: the session machinery extracted from acp.rs into a
  shared module — ONE implementation used by both ACP and serve
  (open_session takes the session's approver as a parameter: ACP wires
  the interactive editor prompt, serve wires a non-interactive
  allowlist; Destructive registers only behind interactive approvers,
  so serve mode keeps Destructive structurally absent). plan_mode
  filters to read-only tools; run_session_turn returns
  (stop_reason, final_text).
- tole serve: hand-rolled HTTP/1.1 on std::net (zero new deps),
  Content-Length bodies, Connection: close, 30s read/write timeouts
  (thread-exhaustion DoS — CodeCora finding), bearer-token auth on
  everything except /health, refuses to start without a token.
- Endpoints: GET /health, POST/GET /sessions, GET
  /sessions/{id}[.status] (non-blocking: try_lock — never blocks on a
  running turn, CodeCora finding), POST /sessions/{id}/prompt
  (serialized per session — concurrent prompts get 409).
- Live E2E: health/auth-matrix (401 without/wrong token)/create/
  prompt (real provider turn)/list/status, all green.

MCP-over-HTTP on the same daemon is the remaining D3 follow-up; issue
#96 stays open for it.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…#135)

* fix: scan-3 triage — all 10 MAJOR findings fixed (Wave 1, issue #132)

1. turn.rs resume: InvalidToolArgs intents (input = bare JSON string)
   are re-settled as errors on replay — never executed with the raw
   string. The Guarded-replay block (approver consult, pre-hook deny)
   and the execution now live inside the object-only else branch.
2. turn.rs resume: opt-in pre-hooks apply on Guarded replay too (the
   hook-deny was bypassable by crashing before settlement).
3. subprocess: sh -c payloads starting with '-' are scanned (leading
   dash on the program token is dash-stripped conservatively).
4. storage: compact() marks the session poisoned when the post-rename
   reopen fails — commits fail loudly instead of vanishing into an
   unlinked inode.
5. openai: tool settlements verify the parent entry is an INTENT before
   emitting a tool message on the wire.
6. serve: request-line + headers capped at 32 KiB (pre-auth memory DoS).
7. session_host: new_session_id gains a monotonic counter suffix (ms+pid
   collisions silently overwrote sessions).
8. serve: sessions capped at 256 with non-busy eviction (long-running
   daemon growth).
9. main.rs: global --workspace/--memory now flow into serve/acp/mcp as
   fallbacks (previously silently ignored).
10. test renamed to match verified behavior (both abort flavors).

MINOR/INFO findings (test hygiene, non-unix paths, evals python) were
addressed in the same pass where touched; the remainder are cosmetic
and tracked on the branch.

* fix: eviction uses try_lock on busy — never blocks the whole map

CodeCora scan round-2: the eviction filter called busy.lock().expect()
while holding the sessions map lock — a turn mid-flight (busy held for
the entire LLM call) would block POST /sessions for the turn's whole
duration, stalling every other route that needs the map. try_lock now
skips locked-busy sessions without blocking; all-busy at capacity
returns 503.

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
ajianaz and others added 8 commits September 28, 2026 16:17
… 2, #133) (#136)

- Connection cap (32): refuse when at capacity — bounds thread/socket
  exhaustion.
- Auth rate-limit per source IP: 10 failures per 60s window → 429
  before the auth check even runs; brute-force hardening on the static
  token.
- 30s read/write IO timeouts per connection (already present, now
  documented alongside the caps).

TLS is documented as a reverse-proxy (nginx/caddy) concern in the
module docs — adding rustls to the hand-rolled HTTP server would be a
different product decision.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat: multi-session MCP over Streamable HTTP (D3-lanjutan, #137)

tole serve --transport mcp: ONE authenticated MCP connection addresses
N durable tole sessions over Streamable HTTP.

- Session tools (session_tools.rs): tole_session_new/prompt/status/list
  ride alongside the registry tools; explicit session_id routing (the
  implicit single-session default is an ergonomics fallback, refused
  when 2+ sessions are open).
- RegistryServer (mcp_server.rs): with_session_resolver — a tool call
  carrying session_id routes to THAT session's registry (its workspace
  jail + approver); the routing key is stripped before execution.
  Session tools themselves stay on the server-level registry.
- Jail-of-jails: client-supplied cwd must resolve INSIDE the server's
  workspace root (CodeCora finding — without it a client could point a
  session at / or /etc).
- Session cap 256 + non-busy eviction (mirrors the REST transport;
  CodeCora finding — the map grew without bound).
- session_prompt is ReadOnly at the server level: the agentic turn
  inside goes through the SESSION's approver; server-level Write made
  the tool uncallable without blanket --allow (found live).
- mcp_http.rs: rmcp StreamableHttpService (tower) served with hyper
  directly (NO axum) behind bearer-token auth; mcp-http feature implies
  shell-tools (CodeCora finding).

Live E2E (real provider): init handshake (SSE) → 15 tools → two
sessions in different cwds → per-session jail reads (ALPHA vs BETA) →
prompt on both (end_turn) → status/list → ambiguous no-id refused →
cwd /etc refused → no-token 401. Tests: 3 new (roundtrip, jail
routing, unknown-session), 41 green under mcp-http features.

* fix: stream SSE response bodies in the MCP HTTP bridge (CodeCora round-2)

Collecting the tower response before sending withheld all SSE bytes
for the entire tool call — a long tole_session_prompt turn would trip
client idle timeouts and drop incremental notifications. The bridge now
forwards body frames as they arrive (BodyStream → BoxBody); mid-stream
read errors surface as connection errors instead of an empty 200.

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
#9) (#139)

- perf_threshold.rs (CI-enforced regression gates): text-only turn,
  10-call tool chain (full intent→effect→settle sandwich per call),
  crash-resume replay — each under a documented 2s budget (observed
  <50ms; ~40x headroom, so failures are real regressions not noise).
  Results recorded in docs/perf.md.
- fuzz_lite.rs (deterministic LCG corpus, no rand dep): 400 hostile
  inputs x 5 tools (traversal, control chars, zero-width, flag
  injection, shell metacharacters, 300-char blobs) must settle
  Ok/Err — never panic; 600 fuzzed argv shapes through
  check_destructive_argv (teardown payloads never pass); truncation
  invariants (40 MiB stream → capped 32 MiB WITH the marked suffix —
  generated child-side, argv limits forbid shipping the payload).
- epics Track C marked DONE; README links docs/perf.md.

E9 acceptance: cross-build CI (#82) ✓, fuzz/panic-free ✓, perf doc ✓,
public-ready (#79/#91) ✓.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
… provider-failure recovery (#106, #85, #84) (#140)

#106 — Cargo.lock committed (rmcp pinned at the verified 3.4.0) and
removed from .gitignore: CI resolved fresh every run while local
machines carried stale locks — 'what compiles' depended on WHEN you
resolved, not the commit. Fresh clone → cargo build --workspace now
reproduces CI's dependency set deterministically (verified from a
clean clone).

#85 — loop guard poll exemption: job_poll with identical input is the
CORRECT pattern (arguments name the job; the RESULT carries the
change) — the old guard aborted healthy 14-minute-render missions at
the 3rd poll. Poll tools now trip at 120 (patience ceiling, still
guards a true stuck-poll); non-poll tools trip at 3 unchanged.
Regression test: 25 identical polls complete; identical non-poll calls
still trip. Live-verified: a 16s detached job polled 6 times to
completion, no trip.

#84 — terminal provider failures now SETTLE the turn (Planning→Final
with a durable error record) instead of wedging at pc=Planning: a
plain 'tole resume <id> "prompt"' works next, matching the E5
crash-resume guarantee. (The malformed-intent replay guard — the other
half of the original wedge — landed with the scan-3 triage #135.)
Tests updated to the new contract: provider-failure lands Final and
the follow-up turn drives cleanly; the mid-flight refusal test now
constructs its wedge state directly. Live-verified: connection-refused
failure → pc Final → prompt resume answers RECOVERED.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.20 to 2.0.21.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](dtolnay/thiserror@2.0.20...2.0.21)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [rmcp](https://github.com/modelcontextprotocol/rust-sdk) from 3.4.0 to 3.4.1.
- [Release notes](https://github.com/modelcontextprotocol/rust-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/rust-sdk/blob/main/release-plz.toml)
- [Commits](modelcontextprotocol/rust-sdk@rmcp-v3.4.0...rmcp-v3.4.1)

---
updated-dependencies:
- dependency-name: rmcp
  dependency-version: 3.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

🔍 Cora AI Code Review

✅ No issues found. Code looks good!


Review powered by cora-code · BYOK · MIT

@ajianaz
ajianaz merged commit 6e1e9db into main Oct 1, 2026
17 checks passed
@ajianaz
ajianaz deleted the chore/release-v0.4.0-main-sync branch October 1, 2026 06:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant