chore(release): v0.4.0 — merge develop into main - #149
Merged
Merged
Conversation
- 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>
#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>
… 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>
🔍 Cora AI Code Review✅ No issues found. Code looks good! Review powered by cora-code · BYOK · MIT |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (#68hardening,#70AllowlistApprover constructors,#73/#76eval 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)
git diff origin/develop HEAD --statshows exactly one file.[0.3.0]section intact in HEAD (git show HEAD:CHANGELOG.md).tole runmission (write_file+read_file with--allow, cora MCP auto-preset 18 tools),tole serveREST (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 acpclean EOF exit.