Repository navigation
Security
What a kind: agent step can reach, and the guards that keep it there.
The model is untrusted in the same way its inputs are: anything it reads (an email, a web page, a tool result) can contain instructions. The design goal is that the playbook author, not the model, decides what can be touched. NoETL enforces that with declared tools, explicit credentials, and treating model output as data.
| Guard | Where | Prevents |
|---|---|---|
auth: required for framework: claude; no env-var or default-key fallback |
AgentSession::start |
a playbook spending credits it never declared |
| Unset / blank credential stops the step before any request | start_agent_session |
sending an empty key; a missing key file looking like a silent no-op |
| API errors report status + message, never the key | AgentSession::next_turn |
keys in logs and --json output |
Tools are only the tools: list; the playbook: binding is never sent to the model |
AgentSession::request_body, runner |
the model choosing what code runs |
Model tool input is passed verbatim, never rendered, and {{ → { {
|
run_agent_tool_call / defang_template_markers
|
text in an email reading playbook variables through the tool playbook's templates |
Failing tools return is_error to the model instead of crashing |
run_agent_tool_call |
partial failures aborting a run halfway |
max_turns (claude) / timeout (claude-code) |
session | runaway loops and runaway cost |
ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN stripped from claude -p
|
run_claude_code |
a subscription run silently billing API credits |
claude-code gets no tools unless allowed_tools lists them; MCP is --strict-mcp-config
|
claude_code_args |
Claude Code using your shell, files, or every MCP server in your user config |
Credential files must be mode 600; aliases are validated ([A-Za-z0-9._-], no leading .) |
local_credentials::resolve |
a shared-readable key file; a templated alias such as ../../x reading arbitrary files |
A google_oauth refresh token is never sent as a bearer / api_key value |
resolve_http_auth_header |
a model-chosen alias shipping a refresh token to the URL a step names |
Gmail tools use format=metadata and the gmail.readonly scope |
playbooks/gmail-triage/tools |
the model reading message bodies, or anything that can change the mailbox |
Up to noetl 5.3.0, the CLI's render_template substituted variables in successive passes. A value that itself contained {{ var }} was expanded by a later pass, so untrusted text templated into a step (e.g. {{ fetch.result }} holding fetched email) could read other variables.
noetl 5.3.1 (noetl/cli#98, executor 0.13.1) makes the CLI renderer single-pass, so it no longer re-expands substituted values. Use 5.3.1 or newer.
A second layer was fixed in noetl 5.3.2 (noetl/cli#100, noetl/ai-meta#477). In --runtime local, shell and http steps used to be rendered by the CLI and then again by noetl-tools (minijinja), so {{ … }} inside data was evaluated twice, and usually erased. Since 5.3.2 the CLI-rendered config is escaped before it reaches the tool, so data arrives unchanged. Verified with a probe: 30/30 runs print the literal {{ workload.secret }}. Use noetl 5.3.2 or newer.
The agent runner still rewrites {{ to { { in model tool input as defence in depth; since 5.3.2 it is no longer load-bearing.
- Store it with
noetl credential set anthropic, which writes~/.config/noetl/credentials/anthropic(mode600, directory700). With noetl 5.1.0, keep it in any600file and exportNOETL_CREDENTIAL_ANTHROPICfrom it. Never in a playbook, a unit file,--set, or shell history. - Set a monthly limit in the Console (Settings → Limits).
- Use a separate key per machine or automation so one can be revoked alone.
The Connect Gmail setup follows these rules: read-only scope, one google_oauth credential per mailbox (gmail-<account>), and metadata only.
Templated aliases. gmail-{{ workload.account }} lets the model choose the account, so it can only pick among credentials you created. A model-chosen account that names a non-Google credential (e.g. anthropic) fails, because google_oauth accepts only refresh-token documents. Never template the alias of a bearer or api_key step from model input.
- Grant read-only scopes / read-only MCP tools for triage. Drafting replies is a separate, explicit tool; sending should stay a human step.
- One credential alias or MCP server per mailbox, named for the mailbox, so logs say which account was touched.
- Keep tool output to sender, subject and snippet. Full bodies cost tokens and widen what injected text can do.
agent-playbooks
Code