Make failure safe
External input can be missing, malformed, stale or hostile. Validate it at the boundary before it affects the system. Treat third-party metadata and agent output as claims to check, not as authority.
Access must be enforced where the action happens. In this illustrative service function, a denial returns before a write. Authentication, input validation and loading the current draft happen before calling it:
async function renameDraft(actor, draft, title, store) {
if (!canEditDraft(actor, draft)) {
return { status: 403, error: "draft-not-editable" };
}
await store.renameIfEditable({
id: draft.id,
expectedVersion: draft.version,
actorId: actor.id,
title,
});
return { status: 200 };
}The store operation must check current permission and version atomically with the write. Otherwise a draft could change after the first check. A conflict or storage failure must become an honest failure response at the service boundary, not a success message.
Plan for interruption
A retry should not accidentally send a second payment, create a second job or duplicate a notification. Use an idempotency key or another mechanism that fits the system. Check what happens when a request succeeds but its response never reaches the caller.
Keep ordinary actions reversible where practical. For consequential actions that cannot be undone, make the effect clear before commitment. Preserve people’s work when something fails. Record consequential changes with enough context to establish who acted, what changed and when, without logging secrets.
Give agents explicit authority
An agent is another client of the product. Prefer documented interfaces that humans can also inspect and use. Give connections only the access they need, and make that access visible and revocable.
Each product defines which actions an agent may perform and which require a person. Enforce those boundaries on the server. When people review an agent’s draft, distinguish its proposal from the verified effect and give them a way to correct or reject it.