Session persistence, context compaction and crash recovery can be outsourced. Compaction strategy, retry semantics and tool-call ordering then leave your hands.
Day 3 covered building your own harness; Day 58 covered building durable execution yourself. In the second half of 2026, both layers became rentable. Anthropic's Claude Managed Agents (in beta since April) runs the agent loop, session storage and sandbox as a hosted service. OpenAI put the Agents API into public beta on September 10, exposing the Codex harness behind an API, and added computer use in a hosted browser at DevDay on September 29. The two products have nearly the same shape: the platform owns the loop and the session log; you supply tools and choose the execution environment. This issue isn't about how to call them. It's about where to draw the line: what is worth handing over, what you can never take back once handed over, and what the "self-hosted sandbox" middle path actually solves.
A managed harness doesn't split model from code. It splits "deciding the next step" from "executing that step".
Anthropic's April 2026 engineering post Scaling Managed Agents splits the architecture into three parts: the brain (Claude plus the harness loop), the hands (sandboxes and tools, unified as execute(name, input) → string), and the session (a durable event log that lives outside the context window). That makes the harness stateless: when an instance dies, a new one calls wake(sessionId), pulls the log back, and continues from the last event. With brain and hands decoupled, a session no longer has to wait for a container to start, and the post reports p50 TTFT down roughly 60% and p95 down over 90%.
The API maps to four objects: Agent (model, system prompt, tools, MCP, skills), Environment (cloud or self-hosted sandbox), Session, and Events. OpenAI's design is isomorphic. The platform manages sessions, orchestration, context compaction and recovery, and compute can run in an OpenAI-hosted sandbox, a partner sandbox, or on your own machines. Neither vendor charges for the harness itself; you pay for model, tool and container usage. The differences are at the edges. OpenAI's September 29 computer use runs in a browser it hosts, and developers still control which sites it can reach. Anthropic adds cron-triggered scheduled deployments and multi-agent orchestration. Match on these edges when choosing; the core loops are close.
Dashed box = the only layer whose location you decide. The first two don't come with you when you switch vendors.
# Minimal Claude Managed Agents loop (SDK adds the beta header: managed-agents-2026-04-01)
from anthropic import Anthropic
client = Anthropic()
agent = client.beta.agents.create(
name="repo-janitor", model="claude-opus-5-5",
tools=[{"type": "agent_toolset_20260401",
"configs": [{"name": "web_fetch", "enabled": False}]}],
)
env = client.beta.environments.create(
name="janitor-env",
config={"type": "cloud",
"networking": {"type": "limited", "allow_package_managers": True}},
)
session = client.beta.sessions.create(agent=agent.id, environment_id=env.id)
with client.beta.sessions.events.stream(session.id) as stream:
client.beta.sessions.events.send(session.id, events=[{
"type": "user.message",
"content": [{"type": "text", "text": "List TODOs in the repo untouched for 90+ days"}]}])
for ev in stream:
if ev.type == "agent.tool_use":
print("[tool]", ev.name)
elif ev.type == "session.status_idle":
break
A managed harness gets upgraded quietly along with the model. That's the selling point, and it's also why your regression tests stop meaning anything.
The same post gives an example. Sonnet 4.5 would wrap up tasks early as it sensed its context limit approaching, so the team added context resets to the harness. On Opus 4.5 the behavior was gone and the resets were dead weight. The lesson: a harness encodes assumptions about what the model can't do on its own, and those assumptions go stale. That is exactly what you pay a managed harness for: the vendor tracks the model for you and removes the outdated patches.
The cost is symmetric. When compaction fires and what it keeps, how many times a failed tool call is retried, how parallel calls are ordered: the platform decides all of these, and the docs say behavior may be refined between releases during the beta. A harness you build is versioned in git. With a rented one, the same agent config on the same task may produce a different tool-call trace next month. What you can do is treat the event log as your observability surface: run a fixed task set regularly and compare trace fingerprints.
Be clear about the few knobs you still hold. Agent configs are versioned (creation returns a version), so treat config changes as releases. Tools can be switched on and off one by one and given permission policies. Web tools take domain allowlists. That is your half, and it belongs in git with your code (the CLI's ant apply records IDs in claude-lock.json). The other half you can observe but not pin.
# Drift sentinel: tool-call sequence + model request count for a fixed task, vs. baseline
import json, hashlib
TOOL_EVENTS = ("agent.tool_use", "agent.custom_tool_use", "agent.mcp_tool_use")
def trace(session_id):
names, model_calls = [], 0
for e in client.beta.sessions.events.list(session_id):
if e.type in TOOL_EVENTS:
names.append(getattr(e, "name", e.type))
elif e.type == "span.model_request_end":
model_calls += 1
return {"tools": names, "model_calls": model_calls,
"fp": hashlib.sha1("|".join(names).encode()).hexdigest()[:10]}
cur, base = trace(SESSION_ID), json.load(open("baseline.json"))
if cur["fp"] != base["fp"] or cur["model_calls"] > 1.5 * base["model_calls"]:
print("harness/model drift: check output quality, then decide whether to rebaseline", cur)
event_delta) are never persisted in history, so after a vendor switch the log can't be replayed and doesn't map onto the new platform's semantics.The loop is rentable. What you should keep is the layer that acts on the outside world, and custom tools are that boundary.
Decide by task shape. Rent when the task runs for tens of minutes to hours, needs to pause and resume, uses mostly standard tools (bash, files, search), and your edge is in prompts and tools rather than the loop itself. Build when the harness is the product (custom compaction, domain-specific planning), when you need to switch model vendors, when you need byte-for-byte replayable audit, or when ZDR or data residency is a requirement.
The most common outcome is in between: rent the loop, keep the side effects. When a custom tool is called, the platform doesn't execute it. The session stops with stop_reason: requires_action and waits for your user.custom_tool_result. That means database writes, emails and charges all execute in your process, so Day 58's idempotency keys still apply, and if you ever switch vendors this layer moves over unchanged.
# Custom tool loop: the platform only sends requests; side effects run on your side, keyed by event id
events_by_id = {}
with client.beta.sessions.events.stream(session.id) as stream:
for ev in stream:
if ev.type == "agent.custom_tool_use":
events_by_id[ev.id] = ev
elif ev.type == "session.status_idle":
if ev.stop_reason.type != "requires_action":
break
for eid in ev.stop_reason.event_ids:
call = events_by_id[eid]
out = ledger.run_once(key=eid, # redelivery executes once
fn=TOOLS[call.name], args=call.input)
client.beta.sessions.events.send(session.id, events=[{
"type": "user.custom_tool_result",
"custom_tool_use_id": eid,
"content": [{"type": "text", "text": out}]}])
requires_action indefinitely. The platform persisted the session, not your loop, so the loop must be able to rebuild events_by_id from event history and resend any missing results. Also, side effects from the built-in bash tool inside the sandbox (a git push, say) bypass this path entirely. Either disable that tool or give it a permission policy that requires confirmation.The worker runs on your machine, and files and egress stay inside your boundary. Tool inputs and outputs still go back to the vendor's control plane.
A Managed Agents self_hosted environment is a work queue. Sessions are enqueued; your environment worker, which needs only outbound HTTPS, claims them and downloads the skills and memory stores. The model runs on Anthropic's side and requests tool calls, which the worker executes locally before posting results back. If no worker is online, sessions wait in the queue rather than fail. The docs are blunt: tool inputs and outputs still flow to Anthropic's control plane, because the model has to see results to decide what to do next. On the OpenAI side, OpenAI has reportedly stated the same thing: bringing your own sandbox changes where tools execute, not where orchestration state lives.
So it solves three things: reaching internal services, keeping files and processes inside your boundary, and routing egress through your own network policy. It doesn't solve residency of session logs and tool results, or ZDR. There are quieter limits too. Self-hosted environments support only memory_store resources; attaching a file or GitHub repo returns a 400. web_search/web_fetch still run on Anthropic's servers and ignore your network policy.
Keep two axes apart. A self-hosted sandbox decides where code executes; an MCP tunnel decides how the vendor reaches MCP servers inside your network. They're independent: a cloud sandbox can use private MCP through a tunnel, and a self-hosted session can use public MCP. To keep both execution and tool access inside your boundary, use both. To skip the tunnel, the worker can wrap the MCP server as custom tools and serve them directly.
# Worker host: environment key only (generated in the Console), no API key
export ANTHROPIC_ENVIRONMENT_KEY="sk-ant-oat01-..."
export ANTHROPIC_ENVIRONMENT_ID="env_..."
sudo mkdir -p /workspace && sudo chown "$USER" /workspace
ant beta:worker poll --workdir /workspace
# Your own machine: confirm at least one worker is polling the queue
ant beta:environments:work stats --environment-id "$ANTHROPIC_ENVIRONMENT_ID"
# only route sessions here once workers_polling ≥ 1
model_calls counter in Point 02 is there to watch.