Prompt injection
Prompt injection is text crafted to hijack an LLM's instructions, an attack class no vendor has solved. Caisson makes no detection claim: @caisson/guardrails runs a fail-closed moderator plus an unconditional secret-shape gate on every input/output leg, and the agent runtime parks any approvalRequired tool call for external approval before it executes, so a hijacked model can't spend or act unchecked.
In code
if (impl.approvalRequired === true) {
if (engine.runState === undefined) {
await engine.append("step.finished", {
stepId,
status: "error",
errorCode: "tool",
});
throw new LoopFailure(
"tool",
`tool "${call.toolName}" requires approval but no runState store was configured`,
);
}
const parkedState: ParkedState = {
stepId,
calls,
callIndex: i,
messages: [...state.messages],
stepsUsed: state.stepsUsed,
creditsSpent: state.creditsSpent,
};
await engine.runState.park({
runId: engine.runId,
toolCallId,
resumeSeq: engine.currentSeq(),
parkedState,
});
throw new LoopParked(toolCallId);
}How it holds
A gated tool call parks, it never runs on the model's word
runToolCallBatch checks impl.approvalRequired before executing any proposed tool call. A gated call appends tool.proposed, records a resumable ParkedState snapshot via RunStateStore.park, and throws LoopParked instead of calling impl.execute, so a model steered by injected text can propose a dangerous call but cannot make it happen without a separate approveToolCall decision.
The credential-shape gate runs unconditionally, injection or not
guard.ts's moderate() calls looksLikeSecret(text) before any moderator, on both the input and output leg, with no policy field to disable it, an injected instruction that tries to get the model to echo out a credential still hits this gate on the way out.
A moderator outage still fails closed
moderateWithDeadline races the configured Moderator against a timeout; a driver throw, rejection, or deadline miss blocks the call unless the policy explicitly sets failOpen: true, a moderator failure can't be used as the injection vector to slip an unmoderated prompt through.
An approved call executes exactly once, even under a race
resumeToolLoop re-validates the pending approval in SQL via RunStateStore.claimResume, a CAS: two concurrent resumes of the same approval can't both execute the tool, and a mismatched or already-claimed toolCallId throws ConflictError before any tool or model logic runs.