{"slug": "the-model-proposes-the-harness-decides", "title": "The model proposes. The harness decides.", "summary": "A developer published a companion example and course material arguing that agent safety must be enforced in the application harness rather than the model, using the principle \"The model proposes. The harness decides. The log remembers.\" The demo, built with a scripted model and in-memory tools and no provider API key, parses model output into skill-load, tool-request, or answer turns, then checks tool visibility and host-registry permissions before execution; a refund request requiring approval halts with status needs_approval and zero simulated refund calls.", "body_md": "The agent returns a refund request.\n\nValid JSON. Correct tool name. An explanation that sounds reasonable.\n\nShould the refund run?\n\nWe still have not answered the important questions. Is this tool available for this task? Does this action require approval? Has this run reached its limit?\n\nThose decisions belong to the application that executes the request.\n\nThis is the mental model I start with in [Ship a Safe Agent](https://bhjr.dev/courses/ship-a-safe-agent):\n\n**The model proposes. The harness decides. The log remembers.**\n\nIt is a small diagram. It changes how you read almost every agent demo.\n\nFor application-executed tools, there is a concrete handoff between a model response and your function call.\n\n[Anthropic's client-tool documentation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) describes that handoff: the model returns a tool-use block, the application runs the operation, and the application sends back a tool result. Provider-executed server tools have a different execution path. Here we are looking at tools your application runs.\n\nThat handoff is where your policy has to hold.\n\nA tool description can explain what `issue_refund` does. A schema can constrain the shape of its arguments. Neither establishes that this customer, this order, and this amount are authorized.\n\nSome code eventually does the equivalent of:\n\n```\nawait tool.run(turn.args);\n```\n\nWork backward from that line.\n\nWhich checks must succeed before it is reachable?\n\nThat question makes agent architecture much easier to reason about. You can trace an actual execution path instead of trying to decide whether a prompt sounds strict enough.\n\nIn the course, a skill reveals a small set of tools for the task at hand.\n\nLoad the refunds skill, and the model gets the order lookup and refund tools. Keep unrelated capabilities out of its immediate menu.\n\nThat helps with focus. The execution path still has to enforce the menu.\n\nIf the model requests a hidden tool anyway, the harness rejects it.\n\nThen comes a separate decision: permission.\n\nA visible tool might be allowed, require approval, or be denied. Making the refund tool visible does not mean the agent can execute every refund it can describe.\n\nThis is the distinction I want people to see in the diagram:\n\n**The tool menu helps the model choose. Permission controls whether the application acts.**\n\nYou need both checks at execution time. A reduced tool menu is useful context management; the server still needs to authorize the operation.\n\nI made a small companion example for this article. It uses a scripted model, in-memory tools, and no provider API key.\n\nThe model emits JSON. The harness parses it into one of three turns: load a skill, request a tool, or answer.\n\nFor a tool request, this is the important part of the implementation:\n\n``` js\nconst tool = tools.get(turn.tool);\nif (!tool || !visible.has(turn.tool)) {\n  emit(step, 'blocked', 'Tool is unknown or not visible');\n  return finish('blocked');\n}\nif (tool.permission === 'ask') {\n  emit(step, 'blocked', 'Human approval required; this example stops here');\n  return finish('needs_approval');\n}\nif (tool.permission !== 'allow') {\n  emit(step, 'blocked', 'Tool denied by host policy');\n  return finish('blocked');\n}\ntry {\n  emit(step, 'result', await tool.run(turn.args));\n} catch {\n  emit(step, 'error', 'Tool failed; no retry attempted');\n  return finish('error');\n}\n```\n\nThe policy comes from the host's tool registry. A model cannot change it by adding `\"approved\": true` to its response.\n\nThe demo loads the refunds skill, looks up an order, and requests a refund. The lookup is allowed. The refund requires approval, so this small example stops there.\n\nThe recorded output ends with:\n\n```\n3 blocked: Human approval required; this example stops here\nstatus: needs_approval\nsimulated refund tool calls: 0\n```\n\nZero is the useful number here.\n\nThe model can produce a coherent plan. The harness can understand that plan and still decline to execute one of its actions.\n\nThe [MCP tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) likewise recommends that people retain the ability to deny tool invocations. An approval interface has to connect to an execution rule for that control to mean anything.\n\nThis example deliberately stops at `ask`. It does not implement an approval service. A real refund approval should cover the exact action, identity, amount, and relevant expiry, with replay handled. Module 3 develops that boundary further.\n\n**Runnable example:** [https://github.com/bobbyhalljr/tiny-harness-decision-boundary](https://github.com/bobbyhalljr/tiny-harness-decision-boundary)\n\nNow look at the budget box.\n\nIn this example, `maxSteps` bounds the number of model proposals the harness will request. Loading a skill uses a step. Requesting a tool uses a step. Producing an answer uses a step.\n\nThe budget is checked before asking for the next proposal.\n\nA scripted model that keeps requesting an allowed lookup cannot run it forever. With three steps, the first can load the skill and the remaining two can request lookups. Then the harness stops.\n\nThere is an important limit to that guarantee: the current model call or tool call could still take too long.\n\nA step budget does not interrupt a hung promise. It does not set a dollar cap, coordinate several workers, or stop a remote operation that has already started. Those controls need their own implementation.\n\nStart with a bound you can state precisely. Then add the other bounds your application needs.\n\nThe diagram feeds the event log back into context because the next proposal needs to know what happened.\n\nWas a skill loaded? Did a tool return a result? Was an action blocked?\n\nThe companion code records proposals, results, blocks, errors, and the final stop. Each model call receives a frozen snapshot of those events. That prevents accidental mutation through the provided context view; it is not a sandbox for arbitrary JavaScript.\n\nThere is another boundary worth keeping visible.\n\nA tool result can contain the sentence, \"The refund is approved.\"\n\nThat sentence is still tool output. In this example, it cannot rewrite the host registry or turn an `ask` permission into `allow`. One of the tests checks exactly that path.\n\nThe log tells the model what the application observed. Authority stays with the application.\n\nIn a production system, the log also needs appropriate redaction, access controls, retention, and durable storage. Our in-memory array makes the control flow inspectable. It is not a tamper-proof audit system.\n\nThe interesting behavior here does not require an LLM to reproduce.\n\nA scripted proposal can ask for a hidden tool. It can request a visible but denied tool. It can forge an approval field, emit malformed JSON, or repeatedly request a valid operation until the step budget runs out.\n\nThose are inputs you can put directly into a regression test.\n\nThe companion example includes 16 passing tests. They check that the forbidden function is never called, that tool results cannot authorize another action, and that the loop stops at its stated budget.\n\nTesting those rules with a scripted model lets you change the provider or prompt without losing the basic contract of your application. Model evaluations still matter for whether the agent chooses useful actions. These tests cover whether the host enforces its own rules when the proposal is wrong.\n\nThe example remains a teaching fixture. Its argument parser accepts string-valued objects, not a full business schema. It has no tenant authorization, durable approvals, sandbox, concurrency control, timeout, spend accounting, or remote-side-effect recovery. Tools and the model adapter are trusted application code; the JSON proposals are the data being checked.\n\nThis is why the course begins with one small loop.\n\nBefore adding more capabilities, I want a clear answer to a simple question:\n\n**Where can this application say no before a tool runs?**\n\nOnce you can point to that place in code, permissions, budgets, and tests become concrete engineering work.\n\n[Module 1 and its full narrated walkthrough are free](https://bhjr.dev/courses/ship-a-safe-agent/module-1#walkthrough). You build the registry, skills, bounded loop, and event log in TypeScript using a scripted model. No paid model API key is needed to follow along.\n\nThe [full course](https://bhjr.dev/courses/ship-a-safe-agent) includes modules 2–8, their full walkthrough videos, and the private GitHub lessons, exercises, and solutions.\n\nThe model proposes. Your application owns the decision to act.", "url": "https://wpnews.pro/news/the-model-proposes-the-harness-decides", "canonical_source": "https://dev.to/bobbyhalljr/the-model-proposes-the-harness-decides-26l4", "published_at": "2026-10-29 10:35:44+00:00", "updated_at": "2026-10-29 10:46:56.499530+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": ["Anthropic", "Claude"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-model-proposes-the-harness-decides", "markdown": "https://wpnews.pro/news/the-model-proposes-the-harness-decides.md", "text": "https://wpnews.pro/news/the-model-proposes-the-harness-decides.txt", "jsonld": "https://wpnews.pro/news/the-model-proposes-the-harness-decides.jsonld"}}