Force Docusign Envelopes to Draft
Rewrites Docusign envelope-creation calls so the envelope is staged as a (status: "sent").
- Direction
- ingress
- Rego package
docusign.ingress.force_draft_envelopes- App
- docusign
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:c1dba6662400ac826544f174979bb864b1bc3c5872db5141910e22ec6982cc2b
docusignforce-draft-envelopesesignhuman-in-the-loopingress
What this policy does
Direction: ingress (tool_pre_invoke)
Default: allow (transform-only — never denies)
Package: docusign.ingress.force_draft_envelopes
What it does
Rewrites Docusign envelope-creation calls so the envelope is staged as a
draft (status: "created") instead of being dispatched
(status: "sent"). With status: "sent", Docusign immediately emails real
recipients a legally binding signature request under your company's Docusign
brand — a hallucinated or injected send is a legal and reputational event,
not a recoverable data event. The safe default is that agents may stage
envelopes but never dispatch them.
Callers whose IdP groups claim includes esign-senders pass through
unchanged, preserving the human-authorized dispatch path. Everyone else has
status forced to "created" — whether it was "sent", missing (the
community servers default to "sent" when omitted), or any other non-draft
value. Calls whose status is already "created" pass through untouched.
All other tools are unaffected.
Compliance alignment
- SOC 2 CC6.3 — supports least privilege and segregation of duties on
the agent channel: the agent holds only the initiate (draft) privilege
and the dispatch privilege stays with humans in the
esign-sendersgroup, preserving the initiate-vs-approve separation for signature transactions (agents prepare, an authorized human sends).
Tool name matching
The policy matches envelope-creation tools by case-insensitive suffix:
*createenvelope— official Docusign MCP ServercreateEnvelope(verified from the developer-docs tool catalog)*create_envelope_from_templateand*create_envelope_from_documents— luthersystems community server (verified from source)
The DTwo gateway prefixes tool names with the configured MCP server name
(e.g. docusign-createEnvelope), and that prefix is not standardized, so
the policy matches on the suffix to stay portable. Both resource.name and
the legacy payload.name alias are checked, so a call missing one of the
two cannot slip past. Verify the exact names your gateway sends with the
dump-input debug technique before relying on this in production.
Argument shape
The policy reads and rewrites the top-level status argument:
- Official
createEnvelopemirrors the eSignature Envelopes:create REST body —statusat the top level ("sent"= dispatch now,"created"= draft), alongsideemailSubject,documents[],recipients.signers[], ortemplateId+templateRoles[]. - Community
create_envelope_from_template/create_envelope_from_documentstake a top-levelstatusthat defaults to"sent"when omitted, which is why a missingstatusis also rewritten to"created".
The transform preserves every other argument via object.union and only
sets status.
Examples
Transformed (agent tried to dispatch)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "docusign-createEnvelope", "type": "tool" },
"subject": { "sub": "agent@example.com", "claims": { "groups": ["staff"] } },
"payload": {
"name": "docusign-createEnvelope",
"args": {
"emailSubject": "Please sign: MSA",
"status": "sent",
"templateId": "tpl-1",
"templateRoles": [{ "roleName": "Signer", "name": "Ana", "email": "ana@acme.com" }]
}
}
}
}
allow = true; transform rewrites status to "created" and preserves all
other arguments — the envelope lands in Drafts, no email goes out.
Allowed unchanged (authorized sender)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "docusign-createEnvelope", "type": "tool" },
"subject": { "sub": "ops@example.com", "claims": { "groups": ["esign-senders"] } },
"payload": {
"name": "docusign-createEnvelope",
"args": { "emailSubject": "Please sign: MSA", "status": "sent" }
}
}
}
allow = true, no transform — the human-authorized dispatch path is
preserved.
Composition
This policy is single-purpose: it governs the create step only. To close the full dispatch surface, pair it with:
- An ingress deny on
updateEnvelopewhen the body carriesstatus: "sent"(sending an existing draft) orstatus: "voided"(irreversible void) for callers outsideesign-senders/contract-ops— without it, an agent can draft here and dispatch viaupdateEnvelope. - An ingress deny on
sendReminderandupdateEnvelopeRecipientsfor non-senders (both generate real email to counterparties). - A recipient-domain allowlist on envelope creation (blocks mis-sends and "add my personal email as a signer" exfiltration).
Known limitations
- Dispatch via other tools is not covered.
updateEnvelope(status: "sent"on a draft),sendReminder, andtriggerWorkflowcan still cause external email; attach the companion policies above. This policy deliberately does one job: force the create step to a draft. - Official-server argument shape is documented, not schema-dumped. The
official tools mirror their documented REST bodies (top-level
status), but Docusign does not publish per-tool MCP JSON schemas on a static page — verify against a livetools/listbefore relying on exact field paths. If your server nests the envelope definition (e.g. underenvelopeDefinition), extend theargsaccessor accordingly. - Nested-
statusdecoy (server-shape dependent, red-team residual). The policy reads and pins only the top-levelstatus. If a server actually honours a nestedenvelopeDefinition.status, two crafted shapes evade the draft-forcing: (a) a decoy top-levelstatus:"created"plus a nestedenvelopeDefinition.status:"sent"— the top-level"created"tripsis_explicit_draft, so the call passes through untouched and the nested"sent"survives; (b) no top-levelstatusplus a nested"sent"— the transform pins top-levelstatus:"created"but the nested"sent"is preserved. Both are covered by tests as documented residuals. This does not affect the official server or the luthersystems community server, whose create tools takestatusat the top level (per the landscape note); it only bites a server that nests the envelope definition. If yours does, extend the accessor to read and pin the nestedstatustoo, and pair with the recipient-domain allowlist companion policy. - Non-canonical
statuskey casing leaves a decoy key. JSON keys are case-sensitive, so aStatus:"sent"/STATUS:"sent"argument is not the top-levelstatusthe policy inspects; the transform therefore fires and injects the canonical lowercasestatus:"created"(the Docusign REST body uses lowercasestatus, which wins). The original mixed-case key is left in the payload as an inert decoy. A hypothetical case-insensitive server that preferred the decoy over the injected canonical key is the residual; the official and community servers use lowercasestatusand are safe. Covered in tests. - Non-object
argspass through. Ifargsarrives as a non-object (e.g. a bare string), the transform is undefined and the call passes through unmodified; such a call carries no valid envelope definition and fails at the Docusign server (documented residual, covered in tests). - A pre-existing
status: "created"is trusted case-insensitively."Created"/"CREATED"are treated as already-draft and left unchanged; Docusign either accepts them as a draft or rejects the call — neither path sends email. - Group name is a placeholder. Replace
esign-senderswith your IdP's group name at import time. Missing subject/claims/groups fail closed for the exemption (no group → not an authorized sender → forced to draft). - Suffix matching misses a trailing segment after the create verb. Tool
names are matched case-insensitively with
endswith, so a name that carries a trailing segment after the create verb (e.g. a version suffix...createEnvelope-v2) would not match and would pass through unmodified withstatus:"sent"intact. No documented Docusign create tool names tools this way — the official server exposescreateEnvelope, the luthersystems community servercreate_envelope_from_template/_from_documents, and the gateway only prepends the configured server name — so this does not affect the real servers. Confirm your gateway's exact tool names with the dump-input debug technique and add any trailing-suffixed variant toenvelope_create_suffixes. Covered in tests. - CData community server is out of scope — it is read-only SQL and has no envelope-creation surface.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package docusign.ingress.force_draft_envelopes
# Transform-only policy: allow everything, and rewrite envelope-creation
# calls to status "created" (draft) unless the caller is an authorized
# sender. Agents stage envelopes; humans dispatch them. Never denies.
default allow := true
# ---------------------------------------------------------------------------
# Configuration placeholders — replace at import time
# ---------------------------------------------------------------------------
# IdP group whose members may dispatch envelopes (status "sent" passes
# through unmodified). PLACEHOLDER: replace with your IdP group name.
esign_senders_group := "esign-senders"
# Envelope-creation tool suffixes. Lower-case; matched case-insensitively.
# - "createenvelope": official Docusign MCP Server createEnvelope (verified
# from the developer-docs tool catalog)
# - "create_envelope_from_template" / "create_envelope_from_documents":
# luthersystems community server (verified from source; its status
# argument DEFAULTS to "sent" when omitted)
envelope_create_suffixes := {
"createenvelope",
"create_envelope_from_template",
"create_envelope_from_documents",
}
# ---------------------------------------------------------------------------
# Shared accessors — every possibly-missing field is read via object.get
# ---------------------------------------------------------------------------
args := object.get(object.get(input, "payload", {}), "args", {})
# Ingress pre-invoke gate. The PARC field is `action`; `kind` is its populated
# legacy alias (same value). Accept EITHER via object.get: if a gateway build
# ever populates only the legacy `kind` (or PARC drops `action`), keying solely
# off `input.action` would silently fail the match and pass a `status:"sent"`
# call straight through — a fail-open dispatch. Restricting to pre-invoke keeps
# the transform off egress hooks, whose payload has `text`, not `args`.
is_pre_invoke if object.get(input, "action", "") == "tool_pre_invoke"
is_pre_invoke if object.get(input, "kind", "") == "tool_pre_invoke"
# Envelope-creation call. The gateway prefixes tool names with the configured
# MCP server name, so match by suffix for portability. Case-insensitive so a
# mixed-case tool name can't slip past. Match on resource.name OR the legacy
# payload.name alias (both populated on tool hooks, same value): a call that
# arrived with an absent resource.name would otherwise miss the match and
# dispatch real signature-request email — a fail-open leak. Reading both via
# object.get also means a missing `resource` object can't error the rule.
is_envelope_create_call if {
is_pre_invoke
some suffix in envelope_create_suffixes
endswith(lower(object.get(object.get(input, "resource", {}), "name", "")), suffix)
}
is_envelope_create_call if {
is_pre_invoke
some suffix in envelope_create_suffixes
endswith(lower(object.get(object.get(input, "payload", {}), "name", "")), suffix)
}
# True when the caller is in the authorized-senders group. Missing subject /
# claims / groups fail closed (no group -> not an authorized sender -> the
# envelope is forced to draft). A groups claim emitted as a bare string is
# not iterated by `some g in`, so it also fails closed.
is_authorized_sender if {
claims := object.get(object.get(input, "subject", {}), "claims", {})
some g in object.get(claims, "groups", [])
g == esign_senders_group
}
# True only when the caller already asked for an explicit draft. Anything
# else — "sent", a missing status (the community default is "sent"), padded
# or unexpected values — gets rewritten. trim_space + lower so "Created "
# still counts as a draft; a non-string status is never treated as a draft.
is_explicit_draft if {
status := object.get(args, "status", "")
is_string(status)
lower(trim_space(status)) == "created"
}
# ---------------------------------------------------------------------------
# Transform: force status "created" on unauthorized envelope creation
# ---------------------------------------------------------------------------
# Preserves every other argument (documents, recipients, templateId,
# emailSubject, ...) and only pins status to "created": the agent's envelope
# lands in Drafts and no recipient is emailed. If args is a non-object the
# object.union is undefined and the call passes through unmodified — such a
# call carries no valid envelope definition and fails at the Docusign server
# (documented residual).
transform := {"transformed_payload": object.union(args, {"status": "created"})} if {
is_envelope_create_call
not is_authorized_sender
not is_explicit_draft
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Related policies
Block Irreversible Docusign Void and Workflow Kills
Denies the irreversible destructive operations on the Docusign agent path:
Cap Docusign Directory and Document Egress
Bounds the two largest data-out channels in the Docusign MCP landscape:
docusigncap-bulk-exportpiidata-minimisationegresssoc2gdpr-ccpa
Docusign: Redact SSN, Bank & Card Values on Egress
Scans the responses of Docusign envelope- and agreement-reading tools and rewrites high-confidence regulated identifiers before the response reaches the…
docusignredact-piitab-valuespiiphipandlpredactionegresssoc2gdpr-ccpa
Guard Docusign External Recipients
Blocks Docusign envelope-creation and recipient-update tool calls when any recipient email address has a domain outside the configured counterparty allowlist.