{"slug": "who-fills-in-the-form-we-only-sign-what-the-model-drafted", "title": "Who Fills In the Form — We Only Sign What the Model Drafted", "summary": "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.", "body_md": "Hmm… for now, I took a quick look around the MCP side of things:\n\nAfter reading the implementation notes too, I think I understand the boundary a little better now.\n\nI 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`\n\nis produced by the surrounding state after the permitted lookups are exhausted, rather than by the model reporting its own uncertainty.\n\nWith 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`\n\nfrom 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`\n\ndirection.\n\nSo I think the most useful thing I found is narrower:\n\nThe core idea seems compatible with current MCP, but`inputSchema.required`\n\nmay be a little too narrow to be the thing that defines the external slot list once full JSON Schema is in scope.\n\nI 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.\n\nFirst, for the explicit questions:\n\n**How far should this apply?**\n\nThe 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`\n\n, `destructiveHint`\n\n, `idempotentHint`\n\n, `openWorldHint`\n\n) 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.\n\n**Would better models make this unnecessary?**\n\nI 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.\n\nThere 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.\n\n**Why not put the checklist in the prompt and ask for JSON?**\n\nThat 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.\n\n**Does this solve wrong-tool selection?**\n\nNot 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.\n\n**What if the verdict record is forged?**\n\nI 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.\n\nFor an execution record, the equivalent might eventually bind something like:\n\n```\nprincipal\n+ server/tool identity\n+ exact final arguments (or canonical digest)\n+ checklist/slot result\n+ schema/policy version\n+ expiry\n+ nonce\n```\n\nYour 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.\n\n`required[]`\n\nis not always the slot list anymoreThe current skeleton intentionally makes the enforceable provider checklist:\n\n```\nfunction getRequiredFields(mcpTool) {\n  if (!mcpTool?.inputSchema) return null;\n  return mcpTool.inputSchema.required ?? [];\n}\n```\n\nand then resolves those fields through the lookup chain.\n\nFor simple flat schemas, that maps very cleanly onto the proposal.\n\nBut 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\n\n`oneOf`\n\n, `anyOf`\n\n, `allOf`\n\n, `if`\n\n/ `then`\n\n/ `else`\n\n, `$defs`\n\n, local `$ref`\n\n, etc.It even has a very useful example:\n\n```\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"isbn\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[0-9]{13}$\"\n    },\n    \"title\": {\n      \"type\": \"string\"\n    },\n    \"author\": {\n      \"type\": \"string\"\n    }\n  },\n  \"oneOf\": [\n    {\n      \"required\": [\"isbn\"]\n    },\n    {\n      \"required\": [\"title\", \"author\"]\n    }\n  ],\n  \"additionalProperties\": false\n}\n```\n\nThe call is valid with either:\n\n```\nisbn\n```\n\nor:\n\n```\ntitle + author\n```\n\nbut the root schema has no `required`\n\narray.\n\nI ran that shape through the current reference skeleton. With the normal `getRequiredFields`\n\n, the provider-field result is effectively:\n\n```\nfields = []\nunknown_count = 0\nexecution_decision = execute\n```\n\nwhile a JSON Schema 2020-12 validator correctly rejects `{}`\n\n.\n\nSo in this case:\n\n```\nunknown_count == 0\n```\n\ndoes not mean:\n\n```\na complete legal invocation exists\n```\n\nI do **not** think this is a counterexample to the external-list idea. It looks more like a useful implementation boundary for the reference skeleton.\n\nIf arbitrary current MCP schemas are in scope, I think the same idea could be generalized from:\n\n```\ninputSchema.required\n        ↓\nslot list\n```\n\nto something like:\n\n```\nschema / provider policy\n        +\ncurrent grounded partial state\n        ↓\nexternally materialized slots\n```\n\nI’ll use “materialized obligations” below just as shorthand for that *current set of slots*; I do not mean to replace your terminology.\n\nThe important property remains unchanged:\n\nThe model still does not decide what is missing.\n\nI tried a deliberately small materializer that understands only this kind of:\n\n```\noneOf(required ...)\n```\n\nschema.\n\nNo model was involved in branch selection or missingness.\n\nFor the `isbn OR (title + author)`\n\nexample, it produced:\n\n| Already grounded | Result |\n|---|---|\n| nothing | branch unresolved |\n`isbn` |\nISBN branch complete |\n`title` |\n`author` remains |\n`author` |\n`title` remains |\n`title + author` |\ntitle/author branch complete |\n`isbn + title + author` |\n`oneOf` conflict |\n\nThat seems useful because it preserves the property I think you care about.\n\nFor example:\n\n```\ntitle = \"Dune\"\n```\n\ndoes not make a model answer:\n\n“I think the missing parameter is author.”\n\nInstead:\n\n```\nschema\n+ grounded partial state\n        ↓\ntitle/author branch\n        ↓\nauthor is mechanically unresolved\n```\n\nThe question then comes from the external remainder.\n\nLikewise, if no unique branch can yet be selected:\n\n```\n{}\n```\n\nthe system does not have to invent a branch merely to produce a flat unknown list. It can preserve:\n\n```\nbranch unresolved\n```\n\nas an external state too.\n\nThis 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.**\n\nThat dependency can still live outside the model.\n\nA minimal implementation path could therefore be:\n\n``` php\nplain root required[]\n    -> current behavior\n\nsimple oneOf / discriminator\n    -> deterministically select a branch from grounded state\n    -> materialize that branch's slots\n\nbranch still ambiguous\n    -> leave branch unresolved\n    -> ask rather than guess\n\nunsupported schema construct\n    -> hold / fail closed\n```\n\nThat seems much cheaper than attempting a general “turn arbitrary JSON Schema into a conversational form” engine.\n\n`getRequiredFields(tool)`\n\nmay eventually need partial stateThere is a small contract issue behind the branch example.\n\nRight now the extension point is essentially:\n\n```\ngetRequiredFields(mcpTool)\n```\n\nBut whether `author`\n\nis an obligation may depend on what has already been grounded.\n\nFor example:\n\n```\npartial state:\n  title = \"Dune\"\n```\n\nis enough to make:\n\n```\nauthor\n```\n\nthe remaining obligation in the example above.\n\nSo, if this schema coverage is desired, the eventual seam may need to look more like:\n\n```\nmaterializeSlots(mcpTool, partialState)\n```\n\nor:\n\n```\nmaterializeObligations(schema, partialState)\n```\n\nrather than being purely a property of the tool definition.\n\nThe crucial restriction would be that `partialState`\n\nitself 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.\n\nSo the dependency would be:\n\n```\nallowed-source lookup\n        ↓\ngrounded partial state\n        ↓\ndeterministic schema/policy evaluation\n        ↓\ncurrent external slot set\n```\n\nnot:\n\n```\nmodel proposes an argument object\n        ↓\nschema tells us what the model forgot\n```\n\nThe first seems much closer to your original principle.\n\nThe other small case was simpler.\n\nSuppose a tool is:\n\n```\nsearch(query, limit=10)\n```\n\nwith:\n\n```\nquery = required\nlimit = optional\n```\n\nand the user explicitly says:\n\n```\nsearch for \"agent safety\", limit 3\n```\n\n`limit`\n\nshould not become an **unknown that blocks execution** if it is absent.\n\nBut if it was supplied, it is still part of the intended call.\n\nThe current skeleton resolves only `requiredFields`\n\ninto `executionState.fields`\n\n, and `executeIfReady()`\n\nconstructs the actual MCP arguments from those fields:\n\n``` js\nconst args = Object.fromEntries(\n  executionState.fields.map(f => [f.name, f.value])\n);\n```\n\nThe code already notes that this assumes a flat `\"field name = argument key\"`\n\ntool.\n\nI tried the optional-field case too. With:\n\n```\nquery = \"agent safety\"\nlimit = 3\n```\n\nthe resulting tool payload was:\n\n```\n{\n  \"query\": \"agent safety\"\n}\n```\n\nThe explicit `limit=3`\n\ndid not reach the final call, because it was not part of the blocking field set.\n\nAgain, this looks less like a problem with the external-list idea than a useful separation to make inside the skeleton:\n\n```\nslot / obligation set\n!=\nfinal argument set\n```\n\nMaybe:\n\n```\nslots\n= things that must be settled for this execution to be allowed\n\narguments\n= everything that will actually be sent to the tool\n```\n\nAn optional argument can therefore be absent from the first set and present in the second.\n\nThat 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.\n\nI mean the final execution object could be produced mechanically from values that were already settled:\n\n```\ngrounded/accepted argument values\n        +\nresolved external slots\n        ↓\ncomplete argument object\n```\n\nfollowed by:\n\n```\nwhole-object JSON Schema validation\n```\n\nbefore execution.\n\nThe distinction became clearer to me when I tried an invalid ISBN too.\n\nAn ISBN value can be *present* — therefore its slot is not empty — while still failing:\n\n```\n\"pattern\": \"^[0-9]{13}$\"\n```\n\nSo:\n\n```\nslot resolved\n```\n\nand:\n\n```\nschema valid\n```\n\nare not the same fact either.\n\nThat suggests four fairly clean responsibilities:\n\n```\n1. Argument collection\n   Where are candidate values allowed to come from?\n\n2. Slot materialization\n   Given schema/policy + grounded partial state,\n   what has to be settled for this invocation?\n\n3. Provenance / condition gate\n   Have those slots actually been settled from permitted sources?\n\n4. Final payload validation\n   Does the complete argument object satisfy the complete inputSchema?\n```\n\nThen:\n\n```\nunknown_count == 0\n```\n\nbelongs to step 3, while:\n\n```\nJSON Schema valid\n```\n\nbelongs to step 4.\n\nThe model does not need authority over either result.\n\nFor a simple tool, all four stages collapse back into almost exactly what the current skeleton already does.\n\nFor a more expressive MCP schema, they stop being identical.\n\nHow I would keep this small rather than chase all of JSON SchemaSo the revised shape I ended up with is roughly:\n\n```\ncandidate action\n      ↓\ncollect only values from permitted sources\n      ↓\nschema/policy + grounded partial state\n      ↓\nmaterialize the current external slot set\n      ↓\nresolve slots / ask for the external remainder\n      ↓\nbuild the complete argument object mechanically\n      ↓\nvalidate the entire object against inputSchema\n      ↓\nverify trigger-time / external conditions\n      ↓\nbind a verdict to that exact execution\n      ↓\nexecute\n      ↓\nrecord what actually happened\n```\n\nThe experiment did not make me think the decision should move back into the model.\n\nIf anything, it suggested a slightly more general place to draw the same external boundary:\n\n```\nnot:\n    the model decides what is missing\n\nand not necessarily:\n    root inputSchema.required[] is the complete list\n\nbut:\n    schema/policy + already-grounded state\n    deterministically determine the slots that must be settled\n```\n\nThen `unknown`\n\ncan remain exactly what you defined it as: **a remainder produced outside the model**.\n\nFor simple tools, that collapses to the current `required[]`\n\nskeleton.\n\nFor 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.", "url": "https://wpnews.pro/news/who-fills-in-the-form-we-only-sign-what-the-model-drafted", "canonical_source": "https://discuss.huggingface.co/t/who-fills-in-the-form-we-only-sign-what-the-model-drafted/179057#post_5", "published_at": "2026-08-25 08:32:30+00:00", "updated_at": "2026-08-25 08:44:10.114579+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "ai-infrastructure"], "entities": ["GitHub", "MCP", "ToolSandbox", "Apple", "Jang-woo-AnnaSoft"], "alternates": {"html": "https://wpnews.pro/news/who-fills-in-the-form-we-only-sign-what-the-model-drafted", "markdown": "https://wpnews.pro/news/who-fills-in-the-form-we-only-sign-what-the-model-drafted.md", "text": "https://wpnews.pro/news/who-fills-in-the-form-we-only-sign-what-the-model-drafted.txt", "jsonld": "https://wpnews.pro/news/who-fills-in-the-form-we-only-sign-what-the-model-drafted.jsonld"}}