Confirmation¶
Policy answers whether an operation may happen. Confirmation answers a different question:
Should the host ask the user before it happens?
Some work is valid according to policy and still deserves explicit awareness — a write to production, a replaced table, an expensive warehouse query, a streaming job that will keep running. Gantry signals ask before proceeding; the host decides how to ask.
What v0 claims, and what it does not¶
Gantry Confirmation v0 is not an authentication or authorization mechanism.
Gantry does not claim:
a trusted human approved this
It claims only:
this operation was marked as requiring confirmation,
and the host application supplied confirmation before execution
That is why this is called confirmation rather than approval, and why no field in
the record names a person. approval implies guarantees around identity, roles,
auditability and separation of duties that v0 does not provide. An authenticated
approval system can later back the same control point without changing the
lifecycle around it.
Asking¶
Confirmation is expressed on the rules that already grant authority:
policy = gantry.Policy(
name="prod-data-agents",
rules=[
gantry.allow.materialize(
sources=["raw.*"],
destinations=["prod.*"],
require_confirmation=True,
confirmation_code="PRODUCTION_WRITE",
confirmation_message="This will write to production data.",
),
],
)
db = gantry.sql.connect("postgres", url=DATABASE_URL, policy=policy)
The decision stays allowed=True. Nothing about it is a refusal:
POLICY_ALLOWED
CONFIRMATION_REQUIRED
A rule that names a source and a destination asks about that pairing. A rule for
raw.* -> prod.* raises no prompt on a raw.* -> scratch.* run, even though it
recognises the source.
Reason codes¶
| Code | For |
|---|---|
SENSITIVE_DESTINATION |
a destination the policy treats as sensitive |
PRODUCTION_WRITE |
a write to production data |
DESTRUCTIVE_OPERATION |
replacing or removing existing data |
HIGH_COST |
work that may be expensive |
LONG_RUNNING_JOB |
a job that keeps running after acceptance |
CUSTOM_POLICY_REQUIREMENT |
anything else; the default when no code is given |
A confirmation_message or confirmation_code without require_confirmation=True
is a configuration error, not a preference — a prompt nobody will ever see is a
mistake worth catching where it is written.
Waiting¶
A parked run is durable, allowed, and has touched nothing:
Run run_01M2…
Admission
✓ allowed
policy: prod-data-agents
✓ read raw.invoices
✗ write prod.totals
Confirmation
! awaiting user confirmation
PRODUCTION_WRITE: This will write to production data.
Decision
AWAITING_CONFIRMATION
AWAITING_CONFIRMATION is not terminal and not a refusal. No engine has been
asked to do anything: no connection opened, no job submitted, no table created.
Answering¶
The host confirms or declines on a separate path from the one the agent proposes on:
run = await materialize(sql)
if run.status is gantry.RunStatus.AWAITING_CONFIRMATION:
answer = input(f"{run.confirmation.message} Continue? [y/N] ")
if answer.lower() == "y":
run = await gantry.runs.confirm(run.id, metadata={"channel": "cli"})
else:
run = await gantry.runs.decline(run.id)
Confirming resumes the same run, with the same immutable proposal, and execution
and verification proceed exactly as they would have. Declining makes the run
terminal at CONFIRMATION_DECLINED with nothing executed.
metadata is for how the host asked — {"channel": "chat"} — not for who
answered. Gantry has no way to verify a name, so it does not record one.
The separation is the point: the agent can discover that confirmation is required, and has no capability to supply it.
proposal path != confirmation path
What the agent sees¶
.tool() reports the requirement and its reasons, and exposes no way to satisfy
them:
{
"run_id": "run_01M2…",
"status": "AWAITING_CONFIRMATION",
"confirmation": {
"status": "required",
"reasons": [
{"code": "PRODUCTION_WRITE", "message": "This will write to production data."}
]
}
}
The tool schema still carries only sql and verify. A tool call that tries to
pass confirmed is refused rather than ignored — an ignored field would look to
the agent like it had worked.
Ordering¶
policy
|
+-- denied ------------------------> POLICY_REJECTED (nothing to confirm)
|
+-- allowed
|
+-- no confirmation ---------> execute
|
+-- confirmation required
|
AWAITING_CONFIRMATION
|
host answers
/ \
confirm decline
| |
execute CONFIRMATION_DECLINED
|
verify
Two consequences worth stating plainly:
- Confirmation cannot override policy. A denial never parks, so there is
nothing for a host to say yes to.
POLICY_REJECTED + confirmedis not a state that exists. - Confirmation cannot weaken verification. A user saying yes is not a
measurement. Execution can still succeed and the run still be
REJECTEDbecause a check failed.
Idempotency and concurrency¶
Confirming twice does not execute twice. The transition out of
AWAITING_CONFIRMATION is a compare-and-set in the run store, so of any number
of repeated or concurrent confirmations exactly one releases the work and the
rest return the run as it now is. Declining something already confirmed — or
confirming something already declined — is refused rather than answered quietly.
Limits of v0¶
- A parked run resumes in the process that proposed it. The run record is durable and readable anywhere; the work waiting on it is not. Confirming from another process raises rather than pretending to resume.
- Each process keeps a bounded number of runs resumable at once
(
gantry.runs.service.MAX_PENDING). A run nobody ever answers stays in the store and stays readable, but eventually stops being resumable rather than pinning its proposal in memory forever. - A custom
RunStoreneedscompare_and_setto back confirmation. One without it raises a clear error instead of making a weaker guarantee quietly.