Tutorial 14 — Putting Human Approval Around Agent Actions¶
Packages:
mas-ctl,mas-runtime,mas-library-standardTime: ~40 min hands-on Goal: Build a budget assistant that can calculate a total but cannot run its calculator until a human or an explicit automated policy approves the action. Prerequisite: Tutorial 0 for installation and LLM access. Tutorial 1 introduces overlays; Tutorial 6 shows the same separation for reusable knowledge rather than operational control.
The problem we will solve¶
Agents often need tools to do useful work. Some tools only read data; others send messages, alter records, spend money, or expose sensitive results. A prompt such as "ask before acting" is not a reliable control because the model itself decides whether to follow it.
This tutorial moves that decision outside the model. We will start with a budget assistant, add a calculator, and then place an approval rule around every tool call. By the end:
- the agent proposes a calculation;
- MAS-Lab pauses before running it;
- an operator approves or rejects the operation;
- the trace records what was requested and how it was resolved.
The complete example lives in this tutorial directory:
docs/tutorials/14-governance-hitl/
├── agent.yaml
└── overlays/
├── tools.yaml
├── governance-interactive.yaml
├── governance-auto-approve.yaml
├── governance-auto-deny.yaml
└── agent-asks-user.yaml
Run every command below from the repository root. Use this pattern when an operation needs an enforceable approval or denial. A prompt instruction is enough only when occasional model judgment is acceptable and no external control or audit trail is required.
Governance and human approval in plain language¶
In MAS-Lab, governance means rules that inspect a proposed operation before or after it crosses the agent boundary. Human-in-the-loop (HITL) means one of those rules pauses the operation and asks a person to decide.
For this tutorial, remember three moments:
agent proposes tool call
|
v
rule asks for approval ---> reject: tool does not run
|
approve
v
tool runs and returns a result
|
v
optional second approval before the model sees the result
The rule is enforced by the runtime. The agent cannot bypass it by changing its answer or tool arguments.
Step 1 — Start with the budget assistant¶
The base agent contains no tool and no approval rule:
apiVersion: mas/v1
kind: Agent
metadata:
name: governed-budget-agent
spec:
description: "Calculate and explain simple project budgets."
context:
role: |
Help the user calculate a project budget. Show the expression used and
explain the result. Use the calculator when it is available.
Validate it from the repository root:
mas-ctl validate docs/tutorials/14-governance-hitl/agent.yaml
At this point the agent can reason about a budget, but it cannot call a calculator.
Step 2 — Add the calculator¶
The first overlay adds the calculator from the sample library. The samples:
prefix names the installed mas-library-samples package, so the manifest can
refer to a reusable tool without depending on a relative filesystem path:
spec:
patch:
tools:
$op:
add:
- ref: samples:tools/calc.tool.yaml
$op: add appends the calculator to any tools already available on the agent.
The same operation is used for the governance rules in the next step.
Run the assistant with that overlay:
mas-ctl chat docs/tutorials/14-governance-hitl/agent.yaml \
-o docs/tutorials/14-governance-hitl/overlays/tools.yaml \
-q "A workshop costs 125 dollars per person for 3 people, plus 42 dollars for materials. Calculate the total."
The agent can now call calc with 125 * 3 + 42 and return $417. Nothing
yet requires approval, so the tool runs as soon as the model requests it.
Step 3 — Require an operator before the tool runs¶
The interactive governance overlay adds a rule on top of the same agent:
spec:
patch:
governance:
$op:
add:
- sample_governance:
hitl_on_tool: true
hitl_on_tool_result: false
hitl_once_per_turn: false
hitl_mode: interactive
The important setting is hitl_on_tool: true: ask a human before every tool
call. Start an interactive session with both overlays:
mas-ctl chat docs/tutorials/14-governance-hitl/agent.yaml \
-o docs/tutorials/14-governance-hitl/overlays/tools.yaml \
-o docs/tutorials/14-governance-hitl/overlays/governance-interactive.yaml \
--trace -i
At the You: prompt, enter:
A workshop costs 125 dollars per person for 3 people, plus 42 dollars for materials. Calculate the total.
When the agent requests calc, the prompt changes from You: to HITL> and
shows the proposed operation. Enter ALLOW to let it run. The agent then
receives the result and answers $417.
Run the same command again and enter BLOCK at HITL>. The calculator
does not run. Depending on the model, the agent may explain that approval was
not granted or continue without the tool. The important result is operational:
the protected action did not execute.
You can use mas-ctl tui instead of mas-ctl chat -i if you prefer the terminal
interface:
mas-ctl tui docs/tutorials/14-governance-hitl/agent.yaml \
-o docs/tutorials/14-governance-hitl/overlays/tools.yaml \
-o docs/tutorials/14-governance-hitl/overlays/governance-interactive.yaml
Step 4 — Decide whether results also need review¶
Some operations return sensitive or untrusted data. To inspect a tool result before it becomes part of the model's working context, change:
hitl_on_tool_result: true
With both switches enabled, one tool use can pause twice:
- before the tool runs, to approve the requested operation;
- after it returns, to approve the data before the model sees it.
This tutorial leaves result review off so the first interaction has one clear approval. Enable it when the returned content itself requires control.
Step 5 — Use explicit decisions in batch and CI¶
An automated job has no person waiting at a terminal. It must state how HITL
requests are resolved. The provided overlays use the same governance rule with
a different hitl_mode.
Auto-approve is useful when a test must exercise the complete tool path:
mas-ctl chat docs/tutorials/14-governance-hitl/agent.yaml \
-o docs/tutorials/14-governance-hitl/overlays/tools.yaml \
-o docs/tutorials/14-governance-hitl/overlays/governance-auto-approve.yaml \
-q "Calculate 125 * 3 + 42."
Auto-deny is useful for checking that a protected operation cannot silently run:
mas-ctl chat docs/tutorials/14-governance-hitl/agent.yaml \
-o docs/tutorials/14-governance-hitl/overlays/tools.yaml \
-o docs/tutorials/14-governance-hitl/overlays/governance-auto-deny.yaml \
-q "Calculate 125 * 3 + 42."
Do not rely on an interactive policy in a process with no terminal. Choose
auto-approve or auto-deny deliberately so the behavior is visible in the
manifest and reproducible in CI.
Step 6 — Inspect the evidence in the trace¶
The --trace run writes traces/events.jsonl beside the agent manifest. Search
for governance and HITL events:
grep -E 'governance|hitl' \
docs/tutorials/14-governance-hitl/traces/events.jsonl
The exact fields may grow over time, but the trace records the proposed operation, the rule that evaluated it, the decision, and the reason. This lets you answer concrete questions after a run:
- Which tool did the agent try to use?
- Was approval required before or after the operation?
- Who or what resolved the request?
- Did the tool run?
This is why governance belongs outside the prompt: the runtime both enforces and records the decision.
Step 7 — Let the agent ask a question when information is missing¶
Policy-driven approval and agent-driven questions solve different problems:
| Situation | Use |
|---|---|
| Every matching operation must be reviewed | A governance rule such as hitl_on_tool |
| The agent cannot continue without a user choice | The request_human_input system tool |
The agent-asks-user.yaml overlay gives the model access to
request_human_input. It also tells the budget assistant to confirm totals over
$400:
mas-ctl chat docs/tutorials/14-governance-hitl/agent.yaml \
-o docs/tutorials/14-governance-hitl/overlays/tools.yaml \
-o docs/tutorials/14-governance-hitl/overlays/agent-asks-user.yaml \
-i
Enter the same $417 workshop request. The agent can calculate the total and
then ask whether it may accept the total. Enter ALLOW or BLOCK when the
HITL> prompt appears. This request originates from the agent's task
instructions. By contrast, the governance rule in Step 3 applies whether or
not the agent wants to ask.
For high-impact actions, use governance as the mandatory control. Use
request_human_input for missing preferences, confirmation, or feedback within
the task.
Step 8 — Understand the available outcomes¶
A governance plugin returns a decision, a rule name, and a human-readable reason. The runtime supports these decisions:
| Decision | Effect |
|---|---|
ALLOW |
Continue normally |
LOG |
Record the decision and continue |
HITL |
Pause and request a human or configured automated response |
BLOCK |
Refuse the operation |
SKIP |
Skip the operation and provide a controlled result |
RETRY |
Ask for another attempt |
MODIFY |
Replace approved parts of the proposed operation |
TERMINATE |
End the session |
BLACKLIST |
Prevent further use of the selected tool in the session |
The sample rule used in this tutorial focuses on HITL. Applications can add
their own plugins for organization-specific rules.
Here is the smallest useful shape of such a plugin:
from mas.runtime.boundary.gov.plugin import EgressDecision
from mas.runtime.boundary.gov.policy import EgressIntentView
from mas.runtime.kernel.config import KernelConfig
from mas.runtime.kernel.coupling import GovDecision
class ReadOnlyToolsOnly:
def evaluate_egress(
self, intent: EgressIntentView, *, config: KernelConfig
) -> EgressDecision:
if intent.op == "TOOL_CALL" and intent.tool_name != "approved-search":
return (
GovDecision.BLOCK,
"read-only-tools",
"only approved-search is allowed",
)
return GovDecision.ALLOW, "read-only-tools", "operation is allowed"
The method receives a description of the proposed operation and returns what
should happen. In production code, use the typed
GovernancePlugin contract
and register the plugin in a library so manifests can refer to it by name.
When several rules are listed, they run in order. An ALLOW lets the next rule
check the operation; a BLOCK stops immediately. A pending human decision can
still be followed by a stricter rule that blocks the operation.
What you built¶
You began with an ordinary budget assistant and added control without changing its agent manifest:
| Layer | Responsibility |
|---|---|
agent.yaml |
Define the budget assistant's job |
tools.yaml |
Make the calculator available |
governance-interactive.yaml |
Require an operator before tool execution |
governance-auto-*.yaml |
Make batch behavior explicit and reproducible |
agent-asks-user.yaml |
Let the agent request information when the task needs it |
The concrete result is a calculator that remains useful but cannot execute outside the decision process selected for the deployment.
Troubleshooting¶
No HITL prompt appears. Confirm that both the tool and governance overlays
are present and use -i. The prompt appears only after the model actually asks
to use a tool.
A non-interactive command blocks or exits. Use one of the auto-decision overlays from Step 5 instead of the interactive overlay.
The trace file is missing. Add --trace to the command. By default, ad hoc
chat traces are written beside the agent under traces/events.jsonl.
Continue with the plugin bindings reference for all manifest options and the governance plugin guide for the built-in implementations.