Who Fills In the Form — We Only Sign What the Model Drafted A developer exploring the execution-state-preflight reference skeleton on GitHub concludes that the model should not own the authority to decide that an execution input is settled, proposing that 'unknown' be produced by surrounding state after permitted lookups are exhausted. The approach is compatible with current MCP but suggests that inputSchema.required may be too narrow once full JSON Schema is in scope, and that better models reduce the frequency of external-layer findings without replacing the layer itself. The developer also notes that binding the verdict to exactly what it authorizes is more important than merely signing it, referencing MCP's August 2026 roadmap on progressive tool discovery. Hmm… for now, I took a quick look around the MCP side of things: After reading the implementation notes too, I think I understand the boundary a little better now. I would not describe this as a way to make the model more careful . The stronger claim seems to be: the model should not own the authority to decide that an execution input is settled in the first place. The model can still do computation, extraction, conversation, and candidate matching, but unknown is produced by the surrounding state after the permitted lookups are exhausted, rather than by the model reporting its own uncertainty. With that reading, most of the boundaries I initially wondered about are already explicitly present in the reference skeleton https://github.com/Jang-woo-AnnaSoft/execution-state-preflight : trusted vs. untrusted instruction segments, the warning against filling userAnswers from LLM parsing, provider descriptions remaining advisory, lookup-hook failures failing closed, deferred execution being re-preflighted, stale measurements, no blind retry after a tool error, tool-selection residual risk, and the future signed decisions + TTL/nonce direction. So I think the most useful thing I found is narrower: The core idea seems compatible with current MCP, but inputSchema.required may be a little too narrow to be the thing that defines the external slot list once full JSON Schema is in scope. I tried two small sanity checks against the current skeleton, plus a minimal deterministic branch resolver. They suggest a separation that might preserve your original model quite well. First, for the explicit questions: How far should this apply? The current repo’s “irreversible actions” scope looks like a reasonable default to me. I would not make every observational call pay the full cost. MCP’s tool annotations readOnlyHint , destructiveHint , idempotentHint , openWorldHint may be useful routing vocabulary, although the MCP tool specification https://modelcontextprotocol.io/specification/2026-07-28/server/tools also warns that annotations from untrusted servers should not be treated as authoritative security information. Would better models make this unnecessary? I think better models can reduce how often the external layer finds a problem, without replacing the layer itself. Better extraction, clarification, or tool selection changes the probability that the first pass succeeds. It does not change who defines the condition “these things must be settled before this execution is valid.” Those seem like separate axes. There is active work on making models better at recognizing missing information — for example, ToolSandbox https://github.com/apple/ToolSandbox explicitly evaluates insufficient-information cases — but that is still different from making the set of required checks external to the model. Why not put the checklist in the prompt and ask for JSON? That seems useful as behavioral guidance, but if the same model decides both what the slots are and whether they are all filled, the authority boundary moves back into the model. So I think this is the clearest distinction in the proposal. Does this solve wrong-tool selection? Not entirely, as you already note. It can eliminate nonexistent tools and reject candidates that fail externally checkable requirements, but selecting the wrong member of a remaining valid candidate set is a different problem. Interestingly, the August 2026 MCP roadmap https://blog.modelcontextprotocol.io/posts/mcp-roadmap/ now calls out degrading tool selection as catalogs grow and is pursuing progressive tool discovery. That looks complementary: discovery can reduce the candidate surface; preflight can govern what happens once a candidate reaches the execution boundary. What if the verdict record is forged? I think the important part is not just “sign the verdict,” but bind the verdict to exactly what it authorizes . The current MCP TypeScript SDK has a useful analogue: requestState https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28 round-trips through the client, so the SDK treats it as untrusted and recommends integrity protection such as HMAC/AEAD, bound to the principal, originating method/parameters, and an expiry. For an execution record, the equivalent might eventually bind something like: principal + server/tool identity + exact final arguments or canonical digest + checklist/slot result + schema/policy version + expiry + nonce Your repo already has signed decisions / TTL / nonce in the future-work boundary, so I mostly see this as an existing MCP example of the same binding problem. required is not always the slot list anymoreThe current skeleton intentionally makes the enforceable provider checklist: function getRequiredFields mcpTool { if mcpTool?.inputSchema return null; return mcpTool.inputSchema.required ?? ; } and then resolves those fields through the lookup chain. For simple flat schemas, that maps very cleanly onto the proposal. But the current 2026-07-28 MCP specification https://blog.modelcontextprotocol.io/posts/2026-07-28/ moved tool schemas to full JSON Schema 2020-12. The oneOf , anyOf , allOf , if / then / else , $defs , local $ref , etc.It even has a very useful example: { "type": "object", "properties": { "isbn": { "type": "string", "pattern": "^ 0-9 {13}$" }, "title": { "type": "string" }, "author": { "type": "string" } }, "oneOf": { "required": "isbn" }, { "required": "title", "author" } , "additionalProperties": false } The call is valid with either: isbn or: title + author but the root schema has no required array. I ran that shape through the current reference skeleton. With the normal getRequiredFields , the provider-field result is effectively: fields = unknown count = 0 execution decision = execute while a JSON Schema 2020-12 validator correctly rejects {} . So in this case: unknown count == 0 does not mean: a complete legal invocation exists I do not think this is a counterexample to the external-list idea. It looks more like a useful implementation boundary for the reference skeleton. If arbitrary current MCP schemas are in scope, I think the same idea could be generalized from: inputSchema.required ↓ slot list to something like: schema / provider policy + current grounded partial state ↓ externally materialized slots I’ll use “materialized obligations” below just as shorthand for that current set of slots ; I do not mean to replace your terminology. The important property remains unchanged: The model still does not decide what is missing. I tried a deliberately small materializer that understands only this kind of: oneOf required ... schema. No model was involved in branch selection or missingness. For the isbn OR title + author example, it produced: | Already grounded | Result | |---|---| | nothing | branch unresolved | isbn | ISBN branch complete | title | author remains | author | title remains | title + author | title/author branch complete | isbn + title + author | oneOf conflict | That seems useful because it preserves the property I think you care about. For example: title = "Dune" does not make a model answer: “I think the missing parameter is author.” Instead: schema + grounded partial state ↓ title/author branch ↓ author is mechanically unresolved The question then comes from the external remainder. Likewise, if no unique branch can yet be selected: {} the system does not have to invent a branch merely to produce a flat unknown list. It can preserve: branch unresolved as an external state too. This is the one place where I would slightly qualify the statement that the slots no longer relate to one another: within a selected branch they can still be independent, but modern schemas can make the existence of one slot conditional on another choice or value. That dependency can still live outside the model. A minimal implementation path could therefore be: php plain root required - current behavior simple oneOf / discriminator - deterministically select a branch from grounded state - materialize that branch's slots branch still ambiguous - leave branch unresolved - ask rather than guess unsupported schema construct - hold / fail closed That seems much cheaper than attempting a general “turn arbitrary JSON Schema into a conversational form” engine. getRequiredFields tool may eventually need partial stateThere is a small contract issue behind the branch example. Right now the extension point is essentially: getRequiredFields mcpTool But whether author is an obligation may depend on what has already been grounded. For example: partial state: title = "Dune" is enough to make: author the remaining obligation in the example above. So, if this schema coverage is desired, the eventual seam may need to look more like: materializeSlots mcpTool, partialState or: materializeObligations schema, partialState rather than being purely a property of the tool definition. The crucial restriction would be that partialState itself contains only values that have already passed the allowed-source rules. Otherwise a model-generated candidate could influence which branch is considered mandatory and the authority would creep back upstream. So the dependency would be: allowed-source lookup ↓ grounded partial state ↓ deterministic schema/policy evaluation ↓ current external slot set not: model proposes an argument object ↓ schema tells us what the model forgot The first seems much closer to your original principle. The other small case was simpler. Suppose a tool is: search query, limit=10 with: query = required limit = optional and the user explicitly says: search for "agent safety", limit 3 limit should not become an unknown that blocks execution if it is absent. But if it was supplied, it is still part of the intended call. The current skeleton resolves only requiredFields into executionState.fields , and executeIfReady constructs the actual MCP arguments from those fields: js const args = Object.fromEntries executionState.fields.map f = f.name, f.value ; The code already notes that this assumes a flat "field name = argument key" tool. I tried the optional-field case too. With: query = "agent safety" limit = 3 the resulting tool payload was: { "query": "agent safety" } The explicit limit=3 did not reach the final call, because it was not part of the blocking field set. Again, this looks less like a problem with the external-list idea than a useful separation to make inside the skeleton: slot / obligation set = final argument set Maybe: slots = things that must be settled for this execution to be allowed arguments = everything that will actually be sent to the tool An optional argument can therefore be absent from the first set and present in the second. That also seems consistent with your “the shape is already there” point. I do not mean that the model should be given a final assembly step. I mean the final execution object could be produced mechanically from values that were already settled: grounded/accepted argument values + resolved external slots ↓ complete argument object followed by: whole-object JSON Schema validation before execution. The distinction became clearer to me when I tried an invalid ISBN too. An ISBN value can be present — therefore its slot is not empty — while still failing: "pattern": "^ 0-9 {13}$" So: slot resolved and: schema valid are not the same fact either. That suggests four fairly clean responsibilities: 1. Argument collection Where are candidate values allowed to come from? 2. Slot materialization Given schema/policy + grounded partial state, what has to be settled for this invocation? 3. Provenance / condition gate Have those slots actually been settled from permitted sources? 4. Final payload validation Does the complete argument object satisfy the complete inputSchema? Then: unknown count == 0 belongs to step 3, while: JSON Schema valid belongs to step 4. The model does not need authority over either result. For a simple tool, all four stages collapse back into almost exactly what the current skeleton already does. For a more expressive MCP schema, they stop being identical. How I would keep this small rather than chase all of JSON SchemaSo the revised shape I ended up with is roughly: candidate action ↓ collect only values from permitted sources ↓ schema/policy + grounded partial state ↓ materialize the current external slot set ↓ resolve slots / ask for the external remainder ↓ build the complete argument object mechanically ↓ validate the entire object against inputSchema ↓ verify trigger-time / external conditions ↓ bind a verdict to that exact execution ↓ execute ↓ record what actually happened The experiment did not make me think the decision should move back into the model. If anything, it suggested a slightly more general place to draw the same external boundary: not: the model decides what is missing and not necessarily: root inputSchema.required is the complete list but: schema/policy + already-grounded state deterministically determine the slots that must be settled Then unknown can remain exactly what you defined it as: a remainder produced outside the model . For simple tools, that collapses to the current required skeleton. For current MCP schemas with conditional structure, the list can become conditional too without giving the model the authority to invent the condition, choose a convenient branch, or declare itself complete.