abridge/tito: TITO gateway — Anthropic agent → session sidecar → TITO training data#122
abridge/tito: TITO gateway — Anthropic agent → session sidecar → TITO training data#122FatPigeorz wants to merge 11 commits into
Conversation
Bring the two upstream repos in-tree as standalone, vendored sidecar projects under sidecars/ — NOT uv workspace members, NOT part of the agentix package. abridge forwards to them over localhost HTTP, so all protocol/ML logic lives here and abridge core stays shape-blind. - cc_convert/ Anthropic<->OpenAI translator (Rust core + axum binary + PyO3) - tito/ TITO pretokenize + session-recording gateway (FastAPI, Miles) Vendored as-is; refactor (cc_convert binary -> in-process code, tito records -> /trace bridge) follows. Upstream licenses/notices preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w fixes
Checkpoint of the in-progress single-transport reliability work together with
the fixes from the multi-agent architecture review. Verified at this state:
pyright clean + 296 core/abridge tests pass.
Architecture-review fixes folded in:
- H1 result-emit task is retained (set + done-callback) so a GC'd task can't
drop a successful call's terminal state.
- H2 the worker's own agentix.* logs are routed off the /log capture pipe
(propagate=False) to kill the broken-pipe feedback loop; user stdlib
logging stays captured (ray-style, no agentix import needed).
- H3 session() always deletes the container even if aclose() raises.
- H4 upstream HTTP status is carried through the abridge tunnel (RemoteSioError
.status_code) instead of collapsing to 502.
- M1 _ensure_sio disconnects a stale client before rebuilding (no duplicate
/rpc socket on a reconnect gap).
- M2 abridge request_timeout is threaded into ns.request (was capped at 300s).
- M4 + doc drift: PROTOCOL.md gains resume/ack + correct test path; codec
docstrings drop ext-types; dead AGENTIX_LOG_BUFFER refs removed; REFACTOR
log wording fixed.
- L cleanups: drop dead make_sio ASGIApp + boot_error frame plumbing; typed
CallCancelled terminal state (no bare CancelledError escaping Ok|Failed);
call:error KeyError guarded; worker cancel enqueued sync; worker drain
join bounded; proxy session teardown no longer masks the body error;
sidecar _proc cleared / unreachable raise documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ /healthz Architecture-review M3: the gateway now routes inference across N backends (sglang/vLLM replicas) via BackendPool — sticky by session_id for prefix-cache locality, report_down on a transport error, forget on session DELETE. Done entirely in non-vendored gateway code (a MilesSessionServer subclass overriding do_proxy + a forget middleware), so VENDORED_MILES_AUDIT stays valid — no vendored file is touched. - config: add backend_urls / routing_policy (validated). - gateway: build the pool (single discovered backend by default, or the explicit backend_urls list); add a /healthz alias so abridge's default Sidecar health probe works (L10). - tests: pool routing/wiring (test_pool_routing.py) + the pool unit tests. 13 tito tests + pyright clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… Docker/Nix) A lightweight SandboxProvider at plugins/providers/uv that materializes a venv with uv and launches the runtime server as a local subprocess — for dev / eval / CI where container isolation isn't needed. - create(): `uv venv` + `uv pip install -e <project>` (or reuse_venv=<path>), then `<venv>/bin/python -m uvicorn agentix.runtime.server.app:app`; raw-socket health wait; delete()/get()/aclose(). - entry-point agentix.provider -> uv = agentix.provider.uv:UvProvider, so the string registry resolves "uv" on an editable workspace install. - The worker imports user code only from site-packages (PYTHONPATH stripped), so the rollout module is pip-installed into the venv, like the Docker bundle. Verified on the cluster: 4 tests + pyright green; drove a full agentic RL rollout (UvProvider -> runtime -> TITO gateway -> sglang) end to end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ecar
The TITO gateway records a trajectory only under
/sessions/{id}/v1/chat/completions, with the id assigned by the gateway at
POST /sessions. A plain Forward posts straight to {target}{path}, so it can't
reach that route. SessionForward closes the gap: it lazily creates the session
(or eagerly via open()), remembers the assigned id, and rewrites every inbound
path to {create_path}/{id}{path} — so an in-sandbox black-box agent keeps
calling an unmodified /v1/chat/completions and the whole rollout lands in one
session. Read .session_id afterward to harvest the trajectory
(GET /sessions/{id}); the session is not deleted on aclose().
- Forward gains a tiny _url_for(path) seam (no behavior change) that
SessionForward overrides; session creation reuses Forward's httpx pool, the
x-session-id / x-request-id stamping, and the error handling.
- Generic, not TITO-specific (create_path / session_id_field configurable);
the vendored Miles session server is untouched, so the M3 audit stays valid.
Tests: 6 new (mock httpx + a live stdlib sidecar mounting /sessions and
/sessions/{id}/v1/chat/completions, proving the real path rewrite). Full abridge
suite 49 passed; pyright clean on the changed source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the multi-lens API design review of the abridge surface: - `.session_id` is now unavailable until the gateway session exists — reading it before `open()`/the first request raises instead of returning Forward's throwaway uuid, so a premature harvest fails loudly rather than hitting a fabricated id. - Fix the class docstring example: it referenced a non-existent `openai_env_for` helper; show the real harvest pattern via `.session_id` and the new `delete_session()`, and make the agent-wiring line runnable. - Document the one-instance-per-rollout reuse contract (all of an instance's calls accumulate into one gateway session; use a fresh instance per rollout). - Add `delete_session()` to reap the server-side session after harvesting (no-op if none; kept separate from `aclose()`/`Proxy.stop` so the default preserves the trajectory; resets so the next call opens a fresh session). Tests: +2 (premature `.session_id` read raises; `delete_session` reaps + resets). Full abridge suite 51 passed; pyright clean on the changed source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Acting on the multi-lens API design review (pre-existing / consistency findings):
- README five-minute example used `upstream_model=`, which the
AnthropicFromOpenAIClient constructor doesn't accept (only `model=`) — fixed so
the first runnable block doesn't TypeError.
- Surface the load-bearing ordering constraint on Proxy.start: registering the
/abridge namespace must precede the runtime client's connect. start() now
catches the low-level RuntimeError and re-raises in this surface's vocabulary
("open the proxy before any other sandbox.remote()/health()"), and the docstring
states it.
- Give OpenAIClient an environ(handle) mirroring the Anthropic clients, with the
OpenAI SDK's required /v1 suffix baked into OPENAI_BASE_URL so callers can't drop
it. All three bundled clients now wire uniformly via env=client.environ(handle).
- Promote the dynamic-route seam to a typed, exported DynamicRoutes Protocol;
_collect_handlers gates on isinstance(client, DynamicRoutes) instead of a
duck-typed getattr, and Client documents both registration paths.
- Distinguish "couldn't reach the sidecar" (now AbridgeError 503) from a relayed
upstream 502, so agent retry logic can tell sidecar-down from bad-gateway.
- The tunnel returns 400 for a body that is present but not a JSON object
(array/string/unparseable) instead of silently coercing it to {}.
- Drop @runtime_checkable from the empty Client marker Protocol (isinstance was
always-true, a false validation signal); construction-time validation stays the
single source of truth.
Tests: +2 (non-object body -> 400; OpenAIClient.environ bakes in /v1); network
error assertion updated to 503. Full abridge suite 53 passed; pyright clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ode training)
Make protocol-conversion and session-routing two ORTHOGONAL, composable
capabilities instead of one fused class:
- Forward.handler(path) exposes a Forward/SessionForward as a plain downstream
Handler — the composition seam (an OpenAI body in, a ClientResponse out).
- AnthropicToOpenAI(downstream, model=...) is the Convert capability and nothing
else: it translates Anthropic /v1/messages ⇄ OpenAI chat-completions and hands
the OpenAI body to `downstream`. Transport-blind. Compose:
tito = SessionForward(gateway_url, paths=["/v1/chat/completions"]) # protocol-blind
proxy = Proxy(AnthropicToOpenAI(tito.handler(), model="qwen3-4b"))
SessionForward never sees Anthropic; AnthropicToOpenAI never sees a session.
Fidelity for strict session backends (the hard part): a session recorder like the
TITO gateway matches each turn's resent assistant message byte-for-byte (role,
content, reasoning_content, tool_calls), but the Anthropic round-trip is lossy — it
drops reasoning_content (Qwen3 emits "\n\n" even with thinking off) and the
per-tool-call `index` sglang stamps. So AnthropicToOpenAI REMEMBERS the exact
assistant message the downstream returned, keyed by the tool-call ids that survive
the round-trip, and replays it verbatim on later turns — making history identical
from the backend's POV and immune to whatever private fields it keeps. Works for
real Claude Code (tool_use ids are stable) and keeps the converter's own concern
(translation fidelity), not the session layer's.
No OpenAI SDK dependency (raw httpx via the downstream Forward).
Verified on GPU (B30Z): a Claude-Code-shaped Anthropic agent with reasoning ON,
driving Qwen3-4B through AnthropicToOpenAI ∘ SessionForward → TITO gateway → sglang:
3 real tool calls (982), 2 records, 1432-token trajectory, 0 hard mismatch, reward
1.0. Tests: +4 (handler() seam; convert over a fake downstream; convert ∘
SessionForward end-to-end; verbatim assistant replay). Full abridge suite 57 passed;
pyright clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Miles) Reimplement the token-in-token-out alignment engine natively under agentix/tito/engine and move the gateway out of sidecars/tito into the plugins/ workspace as the agentix-tito plugin (import agentix.tito). Engine (agentix/tito/engine), all model-agnostic except a small per-model delta: - pretokenize: TITOTokenizer + Qwen3TITOTokenizer — the only Qwen3 difference is a fixed jinja template plus re-inserting the `\n` after `<|im_end|>` the model omits when it stops - compare: special-token-segment mismatch audit - trajectory: LinearTrajectory + SessionRegistry (single-step rollback, from-scratch-vs-accumulated mismatch report) - session_app: FastAPI session routes (3-phase pretokenize -> proxy -> checkpoint) - messages / render / processing / errors No vendored training-framework code and no sglang dependency: the engine tokenizes with transformers + tokenizers + jinja2, isolated to this plugin so agentix core / abridge never pull them. Verified CPU-only with a tiny in-memory tokenizer — pyright clean, 38 tests pass, no model download or GPU. Also: - wire plugins/tito into the root pyright include + extraPaths - drop the inherited standalone-repo scaffolding (docs/, plan.md, vendored-Miles audit, upstream pin, standalone CI, zh README) and rewrite the README + the sidecars/ index for the native plugin - rename lingering miles-isms: config.as_session_args, router_timeout Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@Meirtz could you help me? |
- uv.lock: regenerate so `uv lock --check` passes (was stale vs pyproject changes — transformers/tokenizers/uv/etc). - ruff: sort imports, PEP585/604 annotations, StrEnum for MismatchType/TITOTokenizerType, wrap over-long lines, export SessionForward in agentix.bridge.__all__. - ruff config: exclude vendored `sidecars/` (own upstream repos/CI, mirrors the pyright exclude). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review — verified against the PR head (throwaway worktree; abridge suite 57/57 pass)This is a big, capable PR that delivers work the abridge ARCHITECTURE marks as planned (the TITO gateway path), and the core simplification (single SIO transport, 🔴 Core runtime — confirmed correctness bugs
🔴 New components — confirmed
🟡 Contract hygiene (arch-alignment — all confirmed, not refuted)
🟡 Tests / CI — what green CI does not prove
🟡 abridge layer — minor only (this part is solid)
Recommendation: the direction is good and most of this is well-built, but I'd ask to split it — the core |
|
Hi @FatPigeorz — thanks again for all the work in this PR. Following the 2026-07-01 review, we're landing it as a stack of independent PRs rather than as one 255-file merge. Each stage is ported onto a fresh branch off Please don't push further work onto The stage map and file ownership:
Decisions already settled during review (to save you re-litigating): serialization keeps pickle with a restricted-unpickler allowlist (#116); the msgpack-returns direction was evaluated and closed (#118); If you'd like to own any of stages B–E, comment here and we'll coordinate — otherwise I'll keep porting them in order. |
…A) (#133) Port the abridge SessionForward/AnthropicToOpenAI work from origin/abridge/gateway-tito onto master as the first slice of the #122 split: - SessionForward: forward to session-scoped sidecars (create-on-first- request, path rewrite to /sessions/{id}{path}, delete_session reap). - AnthropicToOpenAI: Anthropic Messages agent over any OpenAI-compatible downstream Handler — protocol translation only, transport-blind, with verbatim assistant replay so session recorders byte-match history. - Forward.handler(): the composition seam — a bound Handler a wrapper can sit on without knowing the transport. - proxy: typed DynamicRoutes protocol for construction-time routes; tunnel 400s on non-object JSON bodies instead of coercing to {}. - sio: RemoteSioError carries the error envelope's status_code, so the tunnel replies with the handler's real status (429/400/503) instead of collapsing every remote failure to 502. (The source branch had this producer half; the port initially took only the consumer.) Review fixes on top of the port (from the pre-PR adversarial review): - _ensure_session accepts any 2xx create response, not just 200. - session_id setter now attaches: assigning an id marks the session ready so the first request doesn't create a fresh session and silently overwrite the assigned id. - Forward.handler() returns a closeable handler and AnthropicToOpenAI delegates aclose() to it, so Proxy.stop reaps the forwarder's httpx pool through the converter composition. - Proxy.session() detects a body failure by catching it, not by probing sys.exc_info() — teardown errors are no longer swallowed for callers that open the session inside an unrelated except block. - delete_session() no longer resurrects a closed pool for one DELETE; it uses a one-shot client, so the documented stop → harvest → reap flow leaks nothing. - Assistant replay memory keys on (id, name, canonical args) per tool call, not ids alone — servers that reuse tool-call ids across turns (TGI's literal "0") no longer corrupt the replayed history. Co-authored-by: FatPigeorz <wjhhhhhha0@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…134) Port plugins/tito from origin/abridge/gateway-tito onto master as the second slice of the #122 split: a self-contained workspace member providing the TITO gateway — a FastAPI session server that keeps a token-aligned trajectory per session and proxies chat completions across a pool of OpenAI-compatible sglang replicas, with a native engine (incremental pretokenization, append-only message validation, single-step rollback, mismatch reporting). No vendored Miles; imports nothing from agentix core beyond the namespace. Planned fixes on top of the port: - BackendPool: a downed replica now recovers — report_up is wired into the proxy success path, and a down mark expires after down_cooldown seconds (half-open) so a replica that is never picked isn't blacklisted forever. - BackendPool: sticky session pins are LRU-bounded (max_pins) — the recommended rollout flow never DELETEs its session (the trajectory must survive for harvest), so the pin map grew without bound. - chat route: force stream=false upstream (drop stream_options) — the TITO flow needs the full JSON completion; a stream:true agent previously got a 500 from json-parsing the SSE bytes. Fixes from the pre-PR adversarial review: - chat route: accept tool-call-only assistant turns with content:null (sglang/vLLM emit them routinely) — previously the first tool call of an agentic rollout 502'd. - trust_remote_code is now an explicit opt-in (config field + --trust-remote-code), never a hardcoded True: loading a tokenizer executes Python shipped inside the checkpoint repo. - trajectory: keep only the two reachable checkpoints (single-step rollback) instead of every full prefix+completion token list — O(turns^2) dead memory per session. - chat route: malformed request JSON is a 400; structurally malformed 200 upstream bodies are a clean 502 (not an unhandled 500). - proxy responses pass the upstream body through verbatim — no JSON re-encode (which 500'd on NaN/Infinity and perturbed byte-sensitive bodies). - prefix-mismatch diagnostic no longer raises a bare ValueError when the new sequence is shorter than the stored checkpoint. - CLI: --backend-url is repeatable (multi-replica pool) and --routing-policy is exposed — the pool was Python-API-only. - packaging: requires-python >=3.11 (StrEnum), drop unused setproctitle dep, sglang-specific docs corrected. - concurrency (from the concurrency review pass): a retry chain that outruns the trimmed checkpoint window now re-renders from scratch instead of merging onto an empty prefix (which silently dropped the whole stored history and recorded a corrupt trajectory); rollback is planned+validated before any mutation, so a rejected (400) request no longer commits truncation side effects; the phase-3 interference guard uses a monotonic version counter (num_assistant was ABA-prone); DELETE sets `closing` inside the lock (a cancelled DELETE no longer wedges the session); the request body is read before taking the session lock (a dribbling upload no longer blocks DELETE); the forget-on-delete middleware matches only the exact session resource. Tests: real HTTP-surface coverage (httpx ASGITransport over the actual FastAPI app: session flow, forced non-streaming, tool-call null content, failover + cooldown recovery, error shapes, verbatim passthrough) on top of the ported unit tests. plugins/tito wired into the uv workspace + pyright include/extraPaths. Co-authored-by: FatPigeorz <wjhhhhhha0@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Cherry-pick of efedacf from origin/abridge/gateway-tito (+ its ruff import-order hunk from 90f970a): a lightweight SandboxProvider at plugins/providers/uv that materializes a venv with uv and launches the runtime server as a local subprocess — for dev / eval / CI where container isolation isn't needed. Registered as entry point 'uv'; wired into pyright include/extraPaths (workspace membership via the existing plugins/providers/* glob). Fixes on top of the port (planned + adversarial review): - stdout drain: the runtime's merged stdout/stderr is drained for the sandbox's lifetime into a bounded tail (8x64KiB). Previously nothing read the pipe after health — asyncio flow control pauses at ~192KiB buffered, so a server that logged past that blocked mid-write and wedged every in-flight rollout. The tail also feeds the exited-before-health diagnostic (was a one-shot read). - that diagnostic path uses asyncio.wait, not wait_for — wait_for cancels the drain on timeout and delete()'s re-await then surfaced a bare CancelledError instead of the RuntimeError diagnostic (repro: a grandchild holding the pipe open past the 2s window). - ports are reserved in-process until the sandbox dies (same _inflight_ports guard as DockerProvider) — the subprocess binds the number seconds after allocation, so concurrent creates could collide: worst case two rollouts silently sharing one runtime. - a failed venv materialization removes its mkdtemp root (a retry loop leaked one partial venv per attempt). - the default uv binary resolves via uv.find_uv_bin() — the packaged wheel's binary is not on PATH under systemd/cron/absolute-path launches; a bare FileNotFoundError also now carries install guidance. - README/docstring examples close the provider (aclose is the only thing that removes a materialized venv). Tests: flood server proving the drain (pre-fix it wedges mid-write), grandchild-held pipe diagnostic, port-collision guard, temp-root cleanup, packaged-uv resolution off-PATH. Co-authored-by: FatPigeorz <wjhhhhhha0@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ion fixes (PR #122 stage D) (#136) Bucket-A slice of the origin/abridge/gateway-tito reliability work (commit b81f2fc), ported surgically onto master with NO wire-contract removals (the HTTP /call fast path, the /log structured bridge, and the codec ext types all stay; those belong to later stages of the #122 split). Server (runtime/server/sio.py): - resume is now definitive: a cached result replays, a running call is left alone, and an evicted/unknown call_id gets call:error type=ResultUnavailable — never silence that hangs the host's remote() forever. - the completed-task transition is atomic w.r.t. resume: the result is stored into pending_results synchronously inside the done-callback (same loop turn as the calls.pop), so a resume can never observe a completing call_id in neither map and misreport a live result as unavailable. (This window existed on the source branch; ported as-fixed.) - result-emit tasks are strongly referenced (emit_tasks set) — a bare create_task in a done-callback can be GC'd before it runs, silently dropping a completed call's result. Applied to both the SIO path and the retained HTTP fast path's timeout branch. Client (runtime/client/client.py): - _ensure_sio tears down a disconnected stale handle before building a fresh one — overwriting it leaked the aiohttp session + reconnect task and could leave a second live /rpc socket. - a malformed call:error with no error payload resolves to a typed MalformedError instead of a bare KeyError escaping remote(). - server-side cancellation now raises CallCancelled (RemoteCallError subclass), not a bare asyncio.CancelledError that reads as local task cancellation. - new try_remote() -> Result[R] (Ok | Failed) for harnesses that branch on outcomes at scale; also on Sandbox. Provider (provider/base.py): - session() deletes the sandbox even when sandbox.aclose() raises — the container and its reserved port must never leak. Worker (runtime/server/worker/process.py): - shutdown drain is bounded (2s): a wedged outbound pipe would hang the queue join forever. - cancel enqueues its Cancelled frame synchronously instead of via an untracked create_task that GC could drop. Adversarial-review fixes on top of the port: - the stale-handle teardown and close() use socketio's shutdown(), not disconnect() — disconnect() is a library no-op on a dropped transport and never stops the background reconnect task, so the leak/double- socket the teardown targets persisted. - submit_http_call attaches its cache+emit callback at submission (not in the timeout branch), so the calls-pop/pending-store transition is in the same completion-callback batch on the HTTP path too; the HTTP-answered path self-acks its cached copy. - the HTTP fast path is opportunistic: transport/status failures fall back to SIO instead of leaking raw httpx errors through remote() and try_remote()'s Result contract. - docs/concepts/remote-calls.mdx + try_remote docstring updated for CallCancelled; client.py __all__ and test_public_exports extended. Docs: PROTOCOL.md documents resume/ack, the no-silent-loss invariant, and the CallCancelled/WorkerExited client mapping. NOT ported (deliberately): the single-transport migration (POST /call removal), reconnection-knob removal, boot_error cleanup, /log rewrite, codec ext-type removal, and every master test the branch's merge-base lag would have deleted (test_extract, test_swe_env, docker retry, client options — all phantom deletions). Co-authored-by: FatPigeorz <wjhhhhhha0@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* security: restricted unpickler on the sandbox->host return boundary (#116, PR #122 stage E) Return values travel host-ward as pickle.dumps(result) and the host decoded them with plain pickle.loads. pickle reconstructs objects by invoking whatever callables a stream names, so decoding a value shaped by a less-trusted sandbox workload (a cloned repo, a generated patch, a benchmark task) is a trust boundary: the returned object can direct reconstruction on the host (#116). Keep pickle as the return codec (the msgpack-returns alternative was evaluated and closed as #118) and decode host-side through a strict allowlist (agentix/runtime/shared/safepickle.py). find_class permits only: - a reviewed set of value types whose construction has no external effect (stdlib data: datetime/decimal/fractions/uuid/collections/ pathlib; builtin containers + exceptions; numpy arrays), and - a small set of inert reconstruction helpers (copyreg, numpy _reconstruct), and refuses everything else WITHOUT importing it. A denylist was rejected as unsound: a stream can name a C-accelerator module (_operator vs operator), a callable produced by one reconstruction step is invoked by the next without passing find_class (so attribute-access helpers must never be admitted), and many ordinary constructors have side effects. A closed allowlist of value types closes all three. First-party types (agentix.*) are trusted by default: the framework and its plugins are the trusted computing base that builds the bundle and runs the sandbox, and their return types (TunnelHandle, BashResult, agent results) are inert. This keeps the framework's own paths (Proxy.start, bash.run, agent adapters) working without setup. A workload's own return types are refused by default; opt in with safepickle.allow_module(prefix) / allow_callable(module, name), or set AGENTIX_PICKLE_TRUST=1 to trust the sandbox fully. A refusal raises agentix.RestrictedUnpickleError. Only the sandbox->host direction is restricted (client._unpickle_value, both the SIO and HTTP result paths). The sandbox-side decode of host-supplied arguments/context stays plain pickle -- the trusted host->sandbox direction. Tests: non-allowlisted callables (subprocess.check_output/Popen, os.system, eval) refused; attribute-access helpers (operator/_operator attrgetter/itemgetter/methodcaller, getattr) refused; refusal does not import the named module; object-dtype numpy arrays gate nested globals; first-party return types (TunnelHandle, BashResult) and permitted value types + builtin exceptions + workload-via-allow_module round-trip; trust bypass; end-to-end refusal over a real remote() call. PROTOCOL.md documents the boundary; RestrictedUnpickleError exported. Closes #116. Co-Authored-By: Claude <noreply@anthropic.com> * docs: design exact-type restricted unpickling * security: add exact pickle type opt-in * test: cover exact pickle policy boundary * security: restrict pickle globals to exact values * test: cover exact first-party pickle values * test: tighten safepickle compatibility assertions * style: format restricted unpickler * docs: add exact-type unpickling implementation plan * test: cover warm pickle extension cache bypass * security: reject pickle extension opcodes * test: make restricted decode regressions harmless * docs: list the six exact default return types in PROTOCOL The unpickling paragraph still described the pre-exact policy: blanket agentix.* prefix trust and the deleted allow_module()/allow_callable() surfaces. Name the six individually registered first-party return types and point extension at allow_type(Class), matching safepickle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: format public export list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: keep agentix-ray-build skill out of the stage E diff The Ray-cluster build notes are unrelated to the restricted unpickler; untrack them so the stage E diff stays on-topic. The file stays on disk as an untracked local note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
|
The split of this PR has fully landed. Status of each piece, with an audit of what did and didn't make it to master: Landed (stages A–E):
In several spots master ended up a superset of the branch: pool down-marking gained cooldown + Deliberately not ported:
Stage F — vendored
Follow-up candidates the audit surfaced (intent from this PR that nothing on master delivers yet):
With A–E merged this PR is superseded — closing it is your call, @FatPigeorz. Thanks for the groundwork; the bulk of it is now on master. 🤖 Generated with Claude Code |
|
Closing: the split has fully landed on master — stages A–E via #133–#137, all follow-ups (#138–#141) resolved via #142–#144 (see the audit comment above for the piece-by-piece mapping). The one deferred piece (experimental cc_convert sidecar preset) remains tracked as #121. Nothing in this branch is unmerged beyond that. |
Summary
Builds out the TITO gateway path — routing an Anthropic-style agent through a session-scoped sidecar and converting the trace into TITO (token-in/token-out) training data for Claude Code — plus the runtime/abridge reliability work underneath it.
What's included
SessionForward+Sidecar/Forwardprimitives — route/v1/...traffic into a session-scoped sidecar, with API-review consistency and boundary fixes.Convert ∘ Session— compose conversion over a session so an Anthropic agent run becomes TITO training data.agentix.tito, dropping the vendored Miles dependency.BackendPool— multi-backend sticky routing (M3) +/healthz.SandboxProvider— runs the runtime from a uv venv, no Docker/Nix required.cc_convert/titosidecar sources and an expanded conversion fixture suite (requests/responses/streams).Test plan
uv run pytestacross the workspacepyrightclean (zero errors)🤖 Generated with Claude Code