Dev.to AI 🤖 Ai 👁 0 📖 6 min read

A proposal is not a reservation: what we learned putting AI agents in a lab's booking chat

Every lab with a shared instrument has the same quiet failure mode. A student asks for an hour on the plate reader. Somebody checks a spreadsheet. Somebody else has a booking that never made it into the spreadsheet. A su

Every lab with a shared instrument has the same quiet failure mode. A student asks for an hour on the plate reader. Somebody checks a spreadsheet. Somebody else has a booking that never made it into the spreadsheet. A supervisor writes "approved" in chat, but by then the conversation has drifted to a different time slot, and nobody is quite sure what was approved.

For the Flip the Switch hackathon we built LabQuorum, a booking workflow where people and AI agents share one team chat and one source of truth. This post covers what we built, the design rule that shaped it, and what happened when a real student used it for the first time. That last part taught us the most.

The one rule: agents propose, people commit

We split the work between two agents with deliberately different powers, running in a Switch room bridged to Mattermost:

  • @labops is the bench manager. Anyone in the room can talk to it. It can list instruments, check training and availability, and create a pending booking request. It cannot commit anything. The commit tool is not in its tool profile at all.
  • @pi-desk is the PI's desk. Only its owner, the principal investigator, can address it. It can retrieve a request, check policy, decline, or ask to commit. A commit only runs after the PI answers a native Switch approval card that shows the exact decision: instrument, requester, local and UTC times, revision and content hash.

Both agents reach the same SQLite ledger through an MCP server. Each agent gets a different tool profile, so the boundary is enforced by what the agent can call, not by what we ask it nicely in a prompt. Every proposal, decline and commit lands in a hash-chained audit log.

The sentence we ended up putting into the agent's instructions, and repeating to ourselves, is: a proposal is not a reservation.

What a good answer looks like

Ask @labops for 60 minutes on INS-01 between 14:00 and 17:00. In our synthetic lab, Dr. Lena Okafor already holds 14:00 to 15:30. A useful reply has to do four things:

  1. Name the actual conflict and who owns it.
  2. Propose the nearest free slot of the same length, respecting the instrument's cleaning gap (here, 15:45 to 16:45).
  3. Name the requester and give a request ID.
  4. Say plainly that the request is pending and nothing is booked.

On the live deployment, that is what happened: LabOps named Lena's 14:00 to 15:30 booking, proposed 15:45 to 16:45, and the request stayed pending with zero commit events and no approval card on the proposer side.

The part that worked before we expected it to

The owner-only boundary on @pi-desk is not something we wrote. Switch enforces it. When a student account addressed @pi-desk asking it to approve a booking, Switch refused the message before the agent ever saw it: the operator had restricted who can address it in that room.

In an isolated certification session on a test ledger, we also exercised the full approval path. The PI answered the native card once and exactly one matching booking and one commit audit event were written. A decline left the request pending. A student pressing Approve on the owner-only card did not settle it. Two prompt-injection probes produced matching cards and no commit.

Then a real student used it

Automated tests told us the ledger was correct. They did not tell us whether the thing was usable. So one of us (a student, not the person who wrote the agents) joined the room cold and typed the request the way a student would:

@rao-labops can I book INS-01 for 60 minutes on the next open day between 14:00 and 17:00?

It went badly in instructive ways.

Read-only lookups asked for permission. Before answering, LabOps raised approval cards for lab_state and check_availability, and the student was able to answer them. The first reply took 7 minutes 47 seconds. A safety prompt on a read is not safety. It trains people to click through prompts, and it makes the one prompt that matters, the commit, less meaningful.

The requester was hard-wired. The proposal tool was scoped to our seeded demo user, Maya. The agent correctly refused to book for the student under someone else's name, but that meant it could not book for the student at all.

Card answers were misread as facts. The student answered the stray cards with "A7 1" and "A9 1". The agent then reasoned about those strings as if they were claims about the student's training.

The PI-desk refusal did hold. That was the one part we had not built ourselves.

What we changed

All three bugs went into a test-first fix run and were merged the same night:

  • Identity comes from the sender, not from arguments. LabOps records the Switch sender's handle as the requester. The student exists in the synthetic lab with real INS-01 training, so the proposal is theirs.
  • Only the binding action prompts. Reads run without approval cards. The single remaining approval point is commit_booking, which belongs to the PI.
  • Card answers are not conversation. The agent no longer treats approval-card responses as statements about the world.

The same pass turned up a quieter problem. On the earlier build, anyone who could message @labops, including a student, could cancel another person's committed booking: cancel_my_proposal took only a booking ID, with no owner check. We reproduced it, then made both cancel_my_proposal and check_availability require and enforce the sender's handle. Conflict replies now include the conflicting interval and a deterministic summary, so the agent cannot drift into vague wording.

Then we ran the same request again, from the same student account, in the same live room. LabOps named Lena's conflicting booking, proposed 15:45 to 16:45 under the student's own name as a pending request, and said "Nothing has been booked." It took 80 seconds instead of almost eight minutes, with zero approval cards. When the student then asked @pi-desk to approve it, Switch refused the message within four seconds.

What we are honest about

LabQuorum ships proposal-only. Binding commits stay off until the remaining checks pass:

  • The one-commit-per-process limit was not exercised live, because no verified Console session ID was available.
  • Three of five injection probes did not capture a request or card, so they count as not proven.
  • No live trace showed which model and reasoning effort the agents actually ran with.

The approve-once pass on a test ledger is real evidence for the approval mechanism. It is not proof that the whole deployed workflow is safe to bind. We would rather ship a correct proposal than a confident reservation.

Lessons we would pass on

  1. Put the boundary in the tool list, not the prompt. The agent that can be talked into anything should not have the dangerous tool.
  2. Prompt for the irreversible step only. Approval fatigue is a security bug.
  3. Derive identity from the platform. If a tool takes "who is asking" as an argument, someone will pass the wrong one.
  4. Test with someone who did not build it. One cold run by a real student found three bugs our suites had passed, and the review that followed found a security hole.
  5. Write down what is not proven. It keeps the demo honest and tells you exactly what to test next.

Try it

The code is open source: github.com/nikhilpravinpise/FlipTheSwitch. It is Python 3.12 with uv, SQLite and the official MCP Python SDK. You can explore the ledger locally without the live room:

uv sync --locked
uv run lq init
uv run lq seed --demo --anchor-date 2026-11-07
uv run lq audit verify

All people and lab records in the demo are synthetic. Built by Nikhil Pise and Darshan Ved for Flip the Switch. Much of this post was drafted by voice with Wispr Flow.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.