cd /news/ai-agents/who-fills-in-the-form-we-only-sign-w… · home topics ai-agents article
[ARTICLE · art-109898] src=discuss.huggingface.co ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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.

read11 min views1 publishedAug 25, 2026

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: 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, butinputSchema.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 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 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 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 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 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:

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:

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.

── more in #ai-agents 4 stories · sorted by recency
── more on @github 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/who-fills-in-the-for…] indexed:0 read:11min 2026-08-25 ·