{"slug": "the-move-the-agent-is-not-allowed-to-make", "title": "The move the agent is not allowed to make", "summary": "A developer built ClauseWatch, a compliance tool that answers questions across contradictory regulations such as the EU AI Act and GDPR by encoding a five-state workflow in Sanity's dataset, where three transitions are reserved for humans and only two are open to an agent. The agent queries the same workflow document it is governed by and reports when it cannot proceed, with a signing desk requiring a typed human name to record decisions. The project is deployed to a Sanity dashboard and its code is public on GitHub.", "body_md": "*This is a submission for the [Sanity Challenge, Path Two: Vibe-Code Something Strange](https://dev.to/challenges/sanity-2026-09-16)*\n\nA signing desk. One screen, one button, and the button does the single thing the agent in\n\nthis system is forbidden to do.\n\nThe project behind it answers compliance questions across regulations that contradict each\n\nother — the EU AI Act says keep your AI system logs for at least six months, the GDPR says\n\nkeep personal data no longer than necessary, and the logs the AI Act mandates contain\n\npersonal data by construction. There is no single number that satisfies both. There is a\n\nperiod someone has to choose and justify.\n\nSo the interesting question is not how the agent answers. It is **who is allowed to decide**,\n\nand how you stop the machine from quietly deciding for you.\n\nThe answer here is a workflow stored as content beside the content it governs:\n\n```\nraised → gathering → review → decided\n                             ↘ dismissed\n```\n\nFive states, five transitions, and every transition names the actor permitted to make it.\n\nTwo are open to an agent: start looking, attach the sources. Three are `human`. Ask the\n\ndataset and it tells you:\n\n```\n{\"name\": \"Deciding a conflict\", \"states\": 5, \"agentCan\": 2, \"humanOnly\": 3}\n```\n\nThe agent reads that same document. When it reaches the end of what it may do, it says so:\n\n```\nCANNOT Sign the decision (review → decided) — reserved for a person\n       Only a person signs. The agent can prepare every part of this and still\n       not make the move.\n```\n\nThat is the strange part, and the part I actually care about. The boundary is not a sentence\n\nin a system prompt that a model can reason its way around on a bad day. It is a row in the\n\ndataset the model queries, rendered in the same Studio a person uses, enforced the same way\n\nfor both.\n\nThe signing desk is where the human side of that boundary lives.\n\n**Who it is for:** whoever ends up holding the compliance file at a small company — usually a\n\nfounder or a lone engineer, not a legal department. They are the ones who get handed an\n\nauditor's question and have nobody to escalate it to. The agent does the reading; they do the\n\ndeciding, and the point of the desk is that the split is visible rather than assumed.\n\n**The app:** [https://www.sanity.io/@o7br4pucm/application/wpdwxiwyygohwmtn59yz92ap](https://www.sanity.io/@o7br4pucm/application/wpdwxiwyygohwmtn59yz92ap)\n\nDeployed to the organisation dashboard. It requires a Sanity login — deliberately, since it\n\nwrites signed decisions, and an anonymous signature is not one. If you are not logged in you\n\nwill get Sanity's login screen rather than the app.\n\n**Everything else is public, no account needed:**\n\n`https://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=*[_type==\"workflow\"][0]`\n`npm install && npm run ask -- --workflow`\nThe desk lists whatever the dataset says is in `review` — it holds no state of its own. For\n\neach conflict it shows both clauses verbatim with their citations, the steps the agent\n\nalready took with timestamps and the actor on each, and then a form.\n\nIf the list is empty when you open it, nothing is waiting: see the note under Sanity Project Details.\n\nThe form asks for what was decided, why, and a name. The signature is **typed**. The App SDK\n\nhas no current-user hook, which turned out to be the right accident: a name that fills itself\n\nin is not a signature, and the whole reason the field exists is that a person chose to put\n\ntheir name against a reading of the law.\n\nSigning writes the decision and its transition record in one edit, so the state cannot move\n\nwithout the evidence of who moved it.\n\n[https://github.com/oleg-vdv/clausewatch](https://github.com/oleg-vdv/clausewatch)\n\n```\nstudio/   schema, desk structure, seed dataset\napp/      the signing desk — Sanity App SDK\nagent/    MCP client, the agent, the workflow gate, a server-rendered viewer\ndemo/     saved agent runs with their tool calls attached\n```\n\nThe workflow is four types: `workflow` holding states and transitions, `workflowState`,\n\n`transition` (which carries `from`, `to`, `actor` and the fields a move requires), and\n\n`transitionRecord` for what actually happened. A `conflict` carries its `state` and a\n\n`history[]` of records.\n\n`agent/src/gate.ts` computes the permitted moves for an actor by reading the workflow. The\n\nsame function serves both sides, and the same transition blocks them differently:\n\n```\nas the agent:  CANNOT Sign the decision — reserved for a person\nas the human:  CANNOT Sign the decision — needs rationale, decidedBy, decidedAt\n```\n\nOne table, two actors, no second set of rules to keep in sync.\n\nClaude Code, in the desktop app, in one sitting. The whole session is 485 messages and it is\n\npublic — including the parts I would rather it were not. What follows is the honest version.\n\nThe single most useful instruction I gave, early, was to treat the dataset as the authority\n\nand stop inventing:\n\nNever fill a gap from your own legal knowledge. If it is not in the dataset or the\n\nknowledge base, say that it is not there.\n\nThat went into the Sanity Context endpoint instructions rather than into my chat, so it\n\nreaches the model before it sees any data. Everything good about the output traces back to\n\nit. The agent's answers started saying \"I cannot tell you\" in the right places, and once that\n\nhappened the product's shape became obvious — if the machine will admit what it does not\n\nknow, the interesting design question is what to do with the gap. Hence the signing desk.\n\n**It published my API token to a public GitHub repo.** I asked it to put the project on\n\nGitHub; it created the repo, pushed, and *then* ran a secret scan — which found a live Sanity\n\ntoken sitting in `.env.example`, where I had pasted it instead of into `.env`. The repo went\n\nprivate within seconds, the token was revoked and replaced, and the history was rewritten to\n\na single clean commit. The order was the bug: the scan should have run before the push, and\n\nthe model said so itself afterwards. I am leaving this in the writeup because a build log\n\nthat only contains the parts that went well is not a build log.\n\n**It confidently corrected Sanity, and was wrong.** Sanity Context flagged a real\n\ncontradiction while indexing: an entry claimed Annex I of the AI Act lists 21 harmonisation\n\ninstruments where the source shows 20. Context also proposed a Section A/B split of 12 and 8.\n\nClaude told me that split was wrong. Then it checked the actual page, found its own reading\n\nwas the unverified one, and said so plainly. The decision recorded in the dataset now states\n\nonly the verified part and excludes the arithmetic neither of us could confirm, because\n\nEUR-Lex blocks automated retrieval.\n\n**A patch script reported success and had changed nothing.** It edited three files with a\n\nPython script that did not verify its replacements; one silently missed, and `--check` kept\n\nprinting two empty lists. It was caught only because I ran the thing from a fresh clone. The\n\nlesson is not \"models are careless\" — it is that an edit which cannot fail loudly will\n\neventually fail quietly.\n\n**Three corpus rebuilds before the knowledge base was right.** `/article/*` as an include\n\npattern did not filter at all: 200 pages including Polish and French translations of the same\n\narticles. Turning on \"Sitemap only\" made it worse — 691 documents, because that switch\n\noverrides include patterns rather than narrowing them. Exact paths worked first time and\n\nevery time. The corpus is now 26 deliberately chosen documents.\n\n**And the bug I would not have found by reading the code.** The role filter used\n\n`count(appliesToRoles) == 0` to mean \"binds everyone\". In GROQ, `count()` of a missing field\n\nis `null`, not `0` — so every GDPR claim silently vanished. The provider's report looked\n\nperfect. Only the deployer's was wrong, and only because the two profiles differ.\n\nTwo-thirds of the way in I asked it to check the submission against the challenge rules. It\n\ncame back with something I had not asked about: a judge cannot hold our organisation token,\n\nso cloning the repo and running it ended at `SANITY_CONTEXT_TOKEN is not set`.\n\nWe split the client. With a token it goes through the Context MCP endpoints; without one it\n\nreads the same public dataset over the query API and says so in the footer and in `--check`,\n\nrather than crediting a source it never touched. Then it cloned from GitHub into a temp\n\ndirectory and ran it with no credentials to prove the fix, which is the check I would have\n\nskipped.\n\nBoth of the things the brief said it wanted to see, and they turned out to be one thing.\n\nThe **workflow** came first, because the process already existed and was hiding in an enum:\n\na conflict had a `resolution` field and nothing that said who was allowed to set it. Pulling\n\nit out into states and transitions took an afternoon and immediately paid for itself — the\n\nagent stopped needing to be told what it may not do, because it could query it.\n\nThe **App SDK** app followed from that. Once `review → decided` was marked `human`, there had\n\nto be somewhere a human makes that move, and the Studio was the wrong place: the Studio is\n\nwhere you edit a document, not where you sign one. The app is 200 lines, reads the workflow\n\nand the conflicts with `useQuery`, and writes with `useEditDocument` and\n\n`useApplyDocumentActions`. It holds no state of its own, which is the property I wanted —\n\nclose the tab mid-decision and nothing is half-saved.\n\n`sanity dev` for an App SDK app binds IPv6 only. The port listens on `[::1]:3333` and\n\nanything resolving localhost to `127.0.0.1` — curl, most preview tooling — sees a dead port\n\nand a blank page with no error. `server: {hostname: '127.0.0.1'}` in `sanity.cli.ts` fixes it.\n\nAlso worth knowing: token permissions cannot be edited after creation. Deploying an App SDK\n\napp needs **Manage SDK Apps**, so if your deploy token lacks it, you are making a new token.\n\n`4yzoidsq`\n`production` (public)\nThe workflow and the conflicts it governs, no token required:\n\n```\nhttps://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=*[_type==\"workflow\"][0]{name,states,transitions}\nhttps://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=*[_type==\"conflict\"]{summary,state,decidedBy,\"moves\":history[]{to,actor,actorKind}}\n```\n\nTwo of the three conflicts are decided, and the second one was signed through the app in\n\nthis post rather than seeded that way. Its history reads:\n\n```\nraised    → gathering   clausewatch-agent (agent)\ngathering → review      clausewatch-agent (agent)\nreview    → decided     Oleg Vdovin (human)   2026-09-19 15:25\n```\n\nWhich means that if you open the signing desk now it will tell you nothing is waiting for a\n\nsignature. That is the desk working, not the desk broken: the conflict in the screenshots\n\nabove is the one that was signed. The third conflict is still `raised` and untouched, so the\n\nagent has somewhere to go next.\n\nThe whole build, 485 messages. I work in Russian, so the conversation is Russian; the tool\n\ncalls, the queries, the terminal output and the model's own English prose are not, and the\n\nslices below are made of those. Each card opens collapsed — click the ▸ for the output.\n\nTerminal output. An import reports success, a document fetch comes back\n\n`{\"documents\":[],\"omitted\":[{\"reason\":\"permission\"}]}`, a one-document probe proves the\n\ndocument ids are the cause, and an anonymous `count(*)` of 18 confirms the fix. This is the\n\nmoment the project stopped being provably public and started being actually public.\n\nThirty seconds that decided how the whole content model works. A script pulls Article 26 and\n\nprints its numbered paragraphs: paragraph 5 is the duty to monitor, paragraph 6 is the duty\n\nto keep logs. The knowledge base had attributed the log duty to 26(5) — its own section\n\nnumbering read as paragraph numbering — and the structured layer is where the verified\n\ncitation now lives.\n\nThe brief suggested Next.js or Astro on the front. I used neither: the app is built on the\n\nSanity App SDK, which the brief named as a bonus, and the public viewer is a small\n\nserver-rendered Node app with no framework at all. If that is a miss against the prompt, it\n\nis a deliberate one — the App SDK is what puts the signing screen next to the content it\n\nsigns.\n\nThe dataset is small: 2 instruments, 9 provisions, 2 requirements, 3 conflicts, 2 system\n\nprofiles, 1 workflow. It is a working core that demonstrates the model, not a compliance\n\nproduct. Nothing here is legal advice.", "url": "https://wpnews.pro/news/the-move-the-agent-is-not-allowed-to-make", "canonical_source": "https://dev.to/olegvdv/the-move-the-agent-is-not-allowed-to-make-3906", "published_at": "2026-09-19 15:21:45+00:00", "updated_at": "2026-09-19 15:53:31.750491+00:00", "lang": "en", "topics": ["ai-agents", "ai-policy", "ai-safety", "developer-tools", "agent-protocols"], "entities": ["Sanity", "EU AI Act", "GDPR", "ClauseWatch", "GitHub", "Sanity App SDK", "MCP"], "alternates": {"html": "https://wpnews.pro/news/the-move-the-agent-is-not-allowed-to-make", "markdown": "https://wpnews.pro/news/the-move-the-agent-is-not-allowed-to-make.md", "text": "https://wpnews.pro/news/the-move-the-agent-is-not-allowed-to-make.txt", "jsonld": "https://wpnews.pro/news/the-move-the-agent-is-not-allowed-to-make.jsonld"}}