How it works
The twelve stages a protected action passes through, what each one can refuse, and what gets recorded.
Every protected action runs the same pipeline, in the same order, every time. There is no fast path and no cache of a previous decision. The order is not arbitrary: each stage is cheaper than the one after it and narrows what the next stage has to consider, and anything that can refuse does so before a credential is ever brokered.
The stage list is a published enum — ACTION_STAGES — and the stage results
come back on every decision in stages[], so what you read here is what you can
inspect at runtime.
Described in text
Stages one to nine each check one thing and each can refuse, and none of them touches anything outside Veefore. Stage ten brokers a credential, which is the first step with an external side effect. That ordering is what makes a blocked action mean the secret never left — see the table below.
The diagram is generated from ACTION_STAGES, so adding a stage to the system
adds it here. The build fails if a new stage arrives without a description.
#The pipeline
| # | Stage | Decides | Refuses with |
|---|---|---|---|
| 1 | REQUEST |
The request is well-formed and in scope | VALIDATION_FAILED, TIMESTAMP_OUT_OF_WINDOW, NONCE_REPLAYED |
| 2 | AUTHENTICATION |
The signature matches a live agent key | AGENT_AUTHENTICATION_FAILED, AGENT_TOKEN_EXPIRED |
| 3 | IDENTITY |
The identity is current, not superseded or revoked | AGENT_IDENTITY_REVOKED, IDENTITY_GENERATION_STALE, PASSPORT_INVALID |
| 4 | LIFECYCLE |
Agent and environment are operational | AGENT_NOT_OPERATIONAL, AGENT_ISOLATED, ENVIRONMENT_PAUSED |
| 5 | CAPABILITY |
The agent holds the capability, and the tool is enabled and authorized | CAPABILITY_NOT_GRANTED, TOOL_NOT_AUTHORIZED, TOOL_DISABLED, DESTINATION_BLOCKED |
| 6 | DELEGATION |
The delegation chain resolves and covers this request | DELEGATION_INVALID, DELEGATION_EXPIRED, AUTHORITY_EXCEEDED |
| 7 | POLICY |
The active policy's decision | ACTION_BLOCKED |
| 8 | RISK |
Risk score and signals, which policy can test | VELOCITY_LIMIT_EXCEEDED |
| 9 | APPROVAL |
Whether a human must decide, or has | APPROVAL_REQUIRED, APPROVAL_NOT_GRANTED, APPROVAL_EXPIRED |
| 10 | CREDENTIAL |
A scoped, short-lived credential is issued to the runtime | INVALID_CREDENTIAL_REFERENCE, CREDENTIAL_SERVICE_UNAVAILABLE |
| 11 | EXECUTION |
The tool actually runs | EXECUTION_FAILED, EXECUTION_TIMEOUT |
| 12 | RESULT |
Limits and masking are applied, the record is sealed | — |
#Why this order
Identity before authority. There is no point resolving what an agent may do until you know which agent it is. Signature verification is stage 2 and nothing downstream runs without it.
Lifecycle before capability. A paused agent is refused by state, not by policy. This matters operationally: pausing an agent must work instantly, and it must not depend on anyone having written a policy that happens to cover it.
Capability before delegation, delegation before policy. Capability is a static grant, delegation is a bounded runtime grant, policy is a conditional rule. Each is strictly narrower and strictly more expensive to evaluate than the one before.
Risk before approval. A policy rule can test risk.score and
risk.signals, so the signals have to be computed before the rule that reads
them. This is why an unusual request can require approval while the identical
request an hour earlier did not.
Credential after every refusal. Stage 10 is the first stage with a side effect outside Veefore. Everything that can say no has already said it. A blocked action never causes a credential to be issued, which is what makes "blocked" mean blocked rather than "blocked, but the secret went out anyway".
Fail closed, at every stage
If a dependency a stage needs is unavailable, the action is refused, not
permitted. Identity verification down means IDENTITY_SERVICE_UNAVAILABLE and
no protected action proceeds. There is no mode in which an error becomes an
allow. See Fail closed.
#What the mode changes
The pipeline is identical in all three modes. The mode only changes what happens to a refusal and whether execution is real.
Described in text
OBSERVE runs every stage, records what enforcement would have decided, and
lets the action execute anyway — so it protects nothing. SIMULATE refuses for
real and executes nothing. ENFORCE refuses for real, and brokers a credential
and executes only when the action is allowed.
OBSERVE |
SIMULATE |
ENFORCE |
|
|---|---|---|---|
| Stages 1–9 run | yes | yes | yes |
| A refusal stops the action | no | yes | yes |
| Credential brokered | yes | no | yes, if allowed |
| Tool actually executes | yes | no | yes, if allowed |
shadow.decision populated |
yes | yes | — |
| Decision recorded | yes | yes | yes |
OBSERVE executes everything
In OBSERVE the pipeline computes a refusal, records it in shadow, and then
lets the action run anyway. That is the whole point of the mode — you learn what
would break before it breaks — but it means OBSERVE provides no protection.
Treat it as instrumentation, not as a safety setting.
SIMULATE is the honest middle ground: the full pipeline, no execution, no
credential, no side effect anywhere. It is what veefore deploy replays your
traffic through during preflight.
#What a decision looks like
json{
"actionId": "act_01JAV7YQ4T2K9M3E8H6R5N1XZ0",
"requestId": "req_01JAV7YQ4T4ZC1P8W2M6B9K3QD",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"mode": "ENFORCE",
"decision": "REQUIRE_APPROVAL",
"outcome": "APPROVAL_REQUIRED",
"allowed": false,
"reason": "Refunds above ₹1,000 require a human approver.",
"explanation": {
"decision": "REQUIRE_APPROVAL",
"policy": { "id": "pol_01JAV…", "key": "refund-limits", "name": "Refund limits", "version": 4 },
"matched_rule": { "id": "rule-refund-ceiling", "name": "Approval above 1000" },
"authority": {
"valid": true,
"max_amount": 500000,
"requested_amount": 120000,
"capabilities": ["refund.create"],
"depth": 1,
"violations": []
},
"delegation": { "valid": true, "chain": ["del_01JAV…"], "reason": null },
"risk": { "level": "MEDIUM", "score": 46, "top_signals": ["Amount is 4.2× this agent's trailing median."] },
"approval": { "required": true, "reason": "Refunds above ₹1,000 require a human approver." },
"reason": "Refunds above ₹1,000 require a human approver."
},
"approval": {
"required": true,
"approvalId": "apr_01JAV…",
"status": "PENDING",
"expiresAt": "2026-10-03T12:40:00.000Z",
"pollAfterMs": 2000
},
"risk": { "level": "MEDIUM", "score": 46 },
"shadow": null,
"limits": { "appliedAmount": null, "maskedFields": [] },
"latencyMs": 38,
"stages": [
{ "stage": "AUTHENTICATION", "outcome": "PASS", "message": "Signature verified.", "latency_ms": 3 },
{ "stage": "POLICY", "outcome": "PASS", "message": "Rule matched: Approval above 1000.", "latency_ms": 2 },
{ "stage": "APPROVAL", "outcome": "PENDING", "message": "Awaiting a human decision.", "latency_ms": 1 }
]
}
Three fields do the work:
decisionis the policy outcome. One ofALLOW,BLOCK,REQUIRE_APPROVAL,STEP_UP_AUTH,LIMIT,MASK.allowedis the single boolean to branch on. It already accounts for mode.explanationnames the policy, the version, the deciding rule, the resolved authority, the delegation chain, the top risk signals and any authority violations. This is what makes a refusal answerable rather than mysterious.
explanation.matched_rules carries every rule that matched, each with a
per-condition trace of expected value, actual value and result. That is the
field to read when a decision is surprising: it shows you not just which rule
won, but which conditions in which rules were true.
LIMIT and MASK are the two decisions that let an action through in modified
form: limits.appliedAmount is the ceiling that was imposed, and
limits.maskedFields lists the response fields that were redacted before the
agent saw them.
#The action record
Every run writes an action record, and the record is the full lineage: the request as received, the resolved authority, every rule consulted, every risk signal that fired, the approval and who decided it, the credential lease (by reference, never by value), the downstream response status, and the final outcome.
bashveefore logs --decision BLOCK --limit 20
or GET /v1/actions/{actionId} for the whole thing. This is the difference
between "the agent was blocked" and "the agent was blocked by rule
rule-refund-ceiling of policy version 4 because amount was 120000 and the
ceiling is 100000, at 12:38:04, and here is the delegation it was acting under".