Human-in-the-loop file output over NextPDF Connect
At a glance
Section titled “At a glance”The human-in-the-loop (HITL) confirmation gate helps prevent unintended
filesystem writes. When you call output_pdf with a file_path, the gate
pauses the call and returns a single-use challenge instead of writing the
file. A human approves it, the agent calls again with the token, and only
then is the file written. Base64 output (no file_path) is not gated.
Install
Section titled “Install”composer require nextpdf/serverBind a transport. By default, output_pdf uses the Approval Required risk
level.
Conceptual overview
Section titled “Conceptual overview”The gate evaluates the effective risk level: the level that remains after any config or caller-identity override. For an Approval Required tool:
- base64 mode —
output_pdfwith nofile_pathis downgraded to Review and allowed without confirmation. It has no filesystem side effect. - file mode —
output_pdfwith afile_pathstays at Approval Required. The gate stores a single-use token bound to the tool name, then returns a challenge. If the target file already exists, the challenge text includes an overwrite warning. The token expires after a fixed window.
In every transport, the gate result is a normal response. A pending approval is a workflow pause, not a transport failure (PHP Standard Recommendation 18 (PSR-18) §3; PSR-18 §p2).
API surface
Section titled “API surface”| Tool | Role | Risk tier |
|---|---|---|
create_pdf | Open the session | Safe |
set_font, add_text | Build content | Caution |
output_pdf (base64) | Return inline; no gate | Review |
output_pdf (file) | Write to disk; gated | Approval Required |
The tool catalog is the source of record, and available tools depend on the installed tier. The HITL risk tiers reference defines the risk ladder and gate.
Code sample — Quick start
Section titled “Code sample — Quick start”create_pdf→ build the content withset_font/add_text.output_pdfwith afile_path→ the gate returns a challenge envelope (the file is not written).- Relay the challenge to the human.
- On approval, call
output_pdfagain with the same arguments plus_confirmation_tokenset to the token from the challenge → the file is written and the session is destroyed.
Code sample — Production
Section titled “Code sample — Production”- Always treat the challenge as a pause. Do not retry in a loop, and do not fabricate a token.
- The token is single-use and bound to the tool name. A token issued for
output_pdfcannot authorize another tool. - If the human rejects, do not call again. To recover the bytes without a file
write, call
output_pdfin base64 mode instead. This requires the session to still exist, so usedestroy: falseon the gated attempt if you expect to need it. - If the token expires before approval, the gate issues a fresh challenge. Relay the new one.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- Token never provided. The operation stays pending. The human must approve, then relay and call again.
- Expired token. A new challenge is issued. Re-relay it.
- Wrong tool. Tokens are tool-bound. Reuse for a different tool fails, and a new challenge is issued.
- Non-absolute path. Rejected before the gate as an invalid path.
- No write permission / disk full. This is a file-write failure after approval. Surface it. Do not retry blindly.
- Session destroyed before re-call. If an earlier
output_pdfuseddestroy: true, the session is gone. Usedestroy: falsewhen you expect the approval round-trip.
Performance
Section titled “Performance”The gate adds a token-store lookup and a round trip for human approval. The
human drives wall time, not the server. Profile is structural.
Security notes
Section titled “Security notes”The gate is the boundary between an agent and the server’s filesystem. It exists because a file write is an irreversible side effect that a human should authorize. The token is single-use, tool-bound, and time-limited. Arguments are deliberately not hashed into it because clients may re-serialize JSON with a different key order. The binding is therefore tool + nonce + TTL, not argument-exact. Do not log the token. Treat it as a one-time secret.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
| A pending approval is a normal response, not a failure. | PSR-18 | §3 | |
| The transport returns a response regardless of result. | PSR-18 | §p2 |
This recipe describes the gate mechanism. It does not assert that the gate makes any operation “secure”. The gate makes a side effect human-authorized.
Commercial context
Section titled “Commercial context”Not applicable — the gate and output_pdf are Core.
Transport availability
Section titled “Transport availability”| Transport | Available | Notes |
|---|---|---|
| MCP (stdio) | Yes | The challenge is surfaced to the host as a tool result for the human to confirm. |
| REST | Yes | The challenge is returned in the response body; re-call with the token. |
| gRPC | Yes | Unary; the challenge is the response message; re-call with the token. |
HITL risk tier
Section titled “HITL risk tier”The risk ladder is Safe (0) → Caution (1) → Review (2) → Approval Required
(3). Only Approval Required tools require human confirmation.
output_pdf is Approval Required. base64 mode downgrades it to Review and
skips the gate. file mode keeps Approval Required and gates the call. The
canonical ladder and policy resolution are in the
HITL risk tiers reference.
Confirmation gate JSON envelope
Section titled “Confirmation gate JSON envelope”The gate result has exactly two shapes, exposed by the server’s confirmation gate:
Allowed (Safe/Caution/Review, or a valid token consumed):
{ "allowed": true }Challenge (Approval Required without a valid token):
{ "allowed": false, "challenge": "⚠️ CONFIRMATION REQUIRED\n\nOperation: output_pdf\nDescription: <tool description>\n\nTo proceed, call output_pdf again with parameter _confirmation_token: \"confirm_<single-use-hex>\"\nExpires in 300 seconds.", "token": "confirm_<single-use-hex>"}When the target file already exists, the challenge text also contains an
overwrite warning line. Re-invoking the same tool with _confirmation_token
set to the token value returns { "allowed": true }, and the operation
continues. The token is single-use and expires after the window stated in the
challenge.