Manifest reference
v0.12 One file, cloakpipe.manifest.yaml, declares everything an agent release may do at its tools. The gate is closed by default: a tool the manifest does not list cannot be called, and an argument a tool does not list cannot be sent.
release: support-agent
version: 2026.10.3
policy: ./policies/support.cedar # optional
tools:
read_ticket:
args:
ticket_id: { type: string, pattern: "^T-[0-9]{4,}$", one_of_context: ["ticket.id"], required: true }
send_email:
args:
ticket_id: { type: string, one_of_context: ["ticket.id"], required: true }
to: { type: string, one_of_context: ["ticket.customer_email"], or_pattern: "@ourcompany\\.com$", required: true }
body: { type: string, max_len: 4000, required: true }
side_effect: true
idempotency_key: "{ticket_id}:{sha256(body)}"
issue_refund:
args:
ticket_id: { type: string, one_of_context: ["ticket.id"], required: true }
amount: { type: integer, min: 0, max: 500, required: true }
side_effect: true
approval:
when: "amount > 100"
approvers: ["slack:#support-leads"]
ttl: 15m
The file is YAML (JSON also works). Unknown keys and duplicate keys are errors. Every rule is checked when the gate starts, so a bad pattern, template or condition stops the gate instead of misbehaving on a live call. A JSON Schema for editors is in the repository at schemas/cloakpipe.manifest.schema.json.
Top level
| Key | Required | Meaning |
|---|---|---|
release: |
yes | the release name. It is the Cedar principal (Release::"support-agent") and keys the policy counters, so a new version does not reset them. |
version: |
yes | your version label, such as 2026.10.3. |
policy: |
no | path to a Cedar policy file, relative to the manifest. See Policy. Without it the gate checks the contract only. |
tools: |
no | a map from tool name (as in tools/call) to its rules. A manifest with no tools allows no calls. |
Tools
| Key | Meaning |
|---|---|
args: |
a map from argument name to its rules. Without args, the tool takes no arguments. An argument not listed is refused as undeclared_argument, without echoing its name. |
side_effect: |
true when a call changes something outside the agent (sends, writes, pays). The gate then tracks each call as an operation, so a retry is never run twice silently. Needs --operations-db. |
effect: |
read, write or irreversible, instead of or alongside side_effect (side_effect: true means write; the two must agree). write and irreversible are side effects. |
idempotency_key: |
a template that names the operation, so retries of “the same email” are recognized. Side-effecting tools only. |
approval: |
when a call needs a signed approval receipt. |
Argument rules
Each argument is optional unless it has required: true; a missing required argument is argument_missing. A rule must restrict the value somehow (at least a type).
| Key | Applies to | Meaning |
|---|---|---|
type: |
any | string, number, integer, boolean, object or array |
required: true |
any | the call must carry it |
enum: |
any | the value must equal one of these |
const: |
any | the value must equal this |
pattern: |
string | a regular expression the whole value must match (it is anchored for you) |
min_len: / max_len: |
string, array | length in characters, or number of items |
min: / max: |
number, integer | inclusive bounds |
items: |
array | the rules for every item |
properties: |
object | the rules for each member; members not listed are refused, and required: true works inside too |
one_of_context: |
any | a list of dotted paths into the task context (ticket.customer_email; a task. prefix is accepted). The value must equal one of the values found there; a context value that is an array matches any of its items. |
or_pattern: |
string, with one_of_context |
a regular expression (unanchored: it may match anywhere) that also lets the value through when no context value matches. |
A value that breaks a rule is argument_invalid; the refusal names the path (/patch/stage) and never the value. For one_of_context, no match is scope_mismatch, and a path missing from the task context (with no or_pattern match) is task_context_missing.
The task context
one_of_context is how a manifest ties a call to the job the agent was given. The task context is a JSON object your orchestrator writes for the current task, passed with --task-context (or CLOAKPIPE_TASK_CONTEXT). The model never writes it:
{ "taskId": "t-1042", "ticket": { "id": "T-1042", "customer_email": "dana@customer.example" } }
taskId is also what approval receipts and derived operation ids are bound to.
idempotency_key
Literal text with {...} placeholders:
| Placeholder | Renders as |
|---|---|
{ticket_id}, {patch.stage} |
the argument at that dotted path: a string as itself, any other value as canonical JSON |
{task.taskId} |
a value from the task context |
{sha256(body)} |
the lowercase hex SHA-256 of the value |
Every argument a template uses must be declared under args. The operation id is op- and 32 hex digits of an HMAC, keyed by a random key kept in the operations database, over the release name, the tool and the rendered key. So "{ticket_id}:{sha256(body)}" makes a retry of the same email for the same ticket the same operation: it is answered from the store, never sent twice, and the same key with different arguments is refused as operation_conflict. Without a template, the id is derived from the task’s taskId and the call fingerprint.
approval
approval:
when: "amount > 100 && currency == 'EUR'"
approvers: ["slack:#support-leads", "pagerduty:refunds"]
ttl: 15m
when: a condition over the call’s arguments. Comparisonspath OP literal, withOPone of>>=<<===!=and a literal that is a number, a quoted string ortrue/false, joined by&&and||(&&binds tighter) with parentheses.pathis a dotted path into the arguments, and each one must be a declared argument (here,amountandcurrency); ordering operators take numbers only. Withoutwhen, every call needs approval. Evaluation fails closed: if a comparison meets a missing value or a value of another type, the condition holds and approval is required.approvers: free-form routing strings. The gate does not send anything to them; it returns them in theapproval_requiredrefusal, with the call fingerprint, for your approval tooling to route.ttl: the longest validity a receipt may have, such as15mor1h30m(unitsd,h,m,s). A receipt whosenotAfterminusapprovedAtis longer is refused asapproval_invalid.
Receipts are signed with cloakpipe approve by a key the gate trusts through --approver-key. See MCP tool gate.
Check and hash a manifest
cloakpipe manifest validate cloakpipe.manifest.yaml
cloakpipe manifest hash cloakpipe.manifest.yaml --json
{
"release": "support-agent",
"version": "2026.10.3",
"manifest_digest": "sha256:c55889deb72d677a13ef2962ba7d6ae97d7a13391c9e484484fde9bb084fb0fa",
"policy_digest": "sha256:fef22029e4619812be1ee292c7aa4b83a1acf754d21b5f59d19658d8fcb1ba3b"
}
The manifest digest is sha256: over the canonical JSON (RFC 8785) of the parsed manifest, so comments, formatting and key order do not change it. The policy digest is the SHA-256 of the policy file’s bytes. Every refusal carries the manifest digest as release, and every ledger entry records both, so an auditor can tell which rules decided each call.
validate also parses the Cedar policy and reports a syntax error with its line. Run it in CI.
The legacy format
v0.11 configured the gate with two files: an AgentRelease (apiVersion: cloakpipe.co/v1alpha1, kind: AgentRelease) and a ToolContract passed with --tool-contract. v0.12 still reads them, with a deprecation warning on stderr, and tells the formats apart by the top-level key (release or apiVersion). The legacy names map as: arguments → args, maxLength → max_len, minimum/maximum → min/max, bindTo: task.x → one_of_context: ["x"], sideEffect → side_effect, requiresApproval: true → approval: {}. The legacy denial argument_not_allowed is now undeclared_argument in both formats.