Security policies
Anyone holding an admin token can author any pipeline, using any enabled
executor — security.allow_shell_checks and security.shell_steps.enabled
are deployment-wide toggles, on or off for everyone, with no way to say “most
pipelines may use shell steps, but anything touching cardholder data may
not.” security_policies is that missing per-pipeline layer: named,
reusable restriction sets an operator defines once and a pipeline opts into
by name, without needing to know or be able to see the policy’s actual
rules.
A pipeline author experiences a policy purely as “my pipeline was rejected, and the error told me which policy and why” or “my pipeline saved and runs fine.” The latter is the compliance signal — if a pipeline declaring a policy runs at all, it adheres to it.
It’s entirely optional. A pipeline with no security_policies: field is
unaffected regardless of what policies exist in the deployment.
v1 scope: which executors a pipeline may use
Section titled “v1 scope: which executors a pipeline may use”Deliberately narrow. A security policy restricts which step executors a
pipeline’s steps may use — nothing else, in this version. Destination
allowlisting (webhook URLs, notification channels), trust/grounding floors,
and cost ceilings are real, likely follow-ups, but each is a materially
different enforcement shape and isn’t built yet — don’t assume a policy can
restrict anything beyond executor: today.
Schema
Section titled “Schema”One YAML file per policy, in security_policies_dir (./security_policies
by default):
name: pci-dss-lite # must match the filename stemdescription: No shell/webhook steps for cardholder-data pipelinesdefault: allow # allow | deny — applies when no rule matchesrules: - deny: { executor: shell } reason: "Shell steps run on the service host — out of scope here" - deny: { executor: webhook } reason: "Arbitrary outbound calls are not permitted from a PCI-scoped pipeline"| Field | Description |
|---|---|
name |
Must match the filename stem (pci-dss-lite.yaml → name: pci-dss-lite). Mismatched files are skipped and logged at startup. |
description |
Free text — shown on GET /security-policies. |
default |
allow or deny — the decision when no rule matches. Defaults to allow. |
rules |
Ordered list. First match wins. |
rules[].allow / rules[].deny |
Exactly one per rule — {executor: <name>}, the only match key in v1. |
rules[].reason |
Required on deny (surfaced in the write-time rejection). Optional on allow. |
This is the same allow/deny, first-match-wins, default-fallback
vocabulary Gateway tool policy already uses —
deliberately not a second rule DSL for the same product, just scoped to one
match key (executor) instead of four.
A pipeline opts in by name:
name: alert-triage-pcistage: productionsecurity_policies: - pci-dss-litesteps: - name: triage executor: gateway # permitted — only shell/webhook are denied above ...Bare names only — there is no way to write policy rules inline in a pipeline’s own YAML. Referencing a name that doesn’t exist in the loaded registry is itself a write-time rejection, not a silent no-op: a pipeline claiming compliance with a policy that doesn’t exist is worse than being ignored, since the false claim is the harm a policy exists to prevent.
The one invariant that matters most
Section titled “The one invariant that matters most”Policy content is filesystem/config.yaml-only — never HTTP-writable, at
any token tier, including admin. If a policy’s rules could be authored
or edited through the same POST/PUT /pipelines trust tier a pipeline
itself is written through, an admin-token caller could simply write a
toothless policy and trivially “comply” with it — the entire “operator-owned
floor” premise collapses. This is the identical posture security.shell_steps.allowed
already has, and the identical reasoning Gateway tool
policy already established for the same
product. Read access is a different story — see Read-only
endpoints below: a pipeline author can see exactly
what a named policy restricts before opting in, they just can’t change it.
Changing a policy needs filesystem/config access and a reload, the same as
adding a shell_steps.allowed entry.
Where it’s enforced
Section titled “Where it’s enforced”Write time is the primary gate. A step’s executor: field is always a
plain literal in pipeline YAML — never Jinja-rendered, unlike a webhook URL
— so every v1 violation is knowable the moment a pipeline is written.
POST/PUT /pipelines (and their /validate counterparts) reject a write
that would violate a declared policy, naming the step, the executor, the
policy, and the rule’s reason:
Step 'notify_partner' uses executor: webhook, denied by security policy'pci-dss-lite': Arbitrary outbound calls are not permitted from aPCI-scoped pipelineRuntime enforcement exists too, as a defense-in-depth backstop — not because an executor can somehow become dynamic, but because write-time validation can’t close two gaps on its own:
- An operator tightens a policy’s rules after a pipeline already passed validation under the old, looser version — the pipeline sits on disk, unchanged, now non-compliant, until its next write.
- A pipeline reaches disk outside the HTTP API entirely — a shipped
sample, a git-synced file dropped straight into
pipeline_config_dir— and was never validated at all.
The runner re-checks at execution time against the live policy registry
(refreshed on every POST /reload/SIGHUP), and a violation aborts the run
immediately, before any step executes — recorded loudly in the run’s audit
trail, never silently folded into a step’s confidence score.
Mandatory-by-stage
Section titled “Mandatory-by-stage”An operator can require certain pipelines to opt in, rather than trusting every pipeline author to remember:
security_policies: required: production: [pci-dss-lite] # every stage: production pipeline must declare this # testing: [] # omit or empty — no requirement at that stageA stage: production pipeline missing a required policy from its own
security_policies: list is rejected at write time — the same boundary
that already enforces the executor restrictions above, not a second
mechanism. There’s no separate /promote endpoint: stage: is just a
field a pipeline author sets via PUT /pipelines, so this check runs at
that identical call site.
Relationship to Gateway tool policy
Section titled “Relationship to Gateway tool policy”Different boundary, same product — worth knowing, no code coupling. A
security policy here denying executor: gateway blocks a pipeline from
running any agent-based step at all: coarse, VectorStep-side,
pre-execution. tool_policy governs
something finer, entirely inside the Gateway, only reachable once a
gateway step is already running: which tools that agent may call. A
pipeline permitted to use executor: gateway by its security policy can
still be further restricted by whatever tool_policy the Gateway
deployment enforces — the two never conflict, since they gate different,
non-overlapping moments, but the error message tells you which layer
rejected you: a security_policies denial names the step’s executor and
the VectorStep policy; a tool_policy denial (surfaced in the agent’s own
trace) names the tool and the Gateway policy. Fixing one means editing
security_policies: in the pipeline YAML; fixing the other means editing
what tools the agent is scoped to, in a different repo entirely.
Read-only endpoints
Section titled “Read-only endpoints”GET /security-policies # name, description, default, rule count per policyGET /security-policies/{name} # full rules — the transparency a pipeline # author needs before opting inRole: viewer for both. No POST/PUT/DELETE exists for this
prefix at any token tier — see The one invariant that matters
most above.
GET /pipelines/{name} also gains a security_policy_status field: for
each policy the pipeline declares, whether it currently resolves (exists in
the loaded registry) and passes — the same compliance signal GET /security-policies/{name} implies, without cross-referencing two endpoints
by hand.
Restart or reload required
Section titled “Restart or reload required”Like security.shell_steps.allowed, policy files are read once at startup
and refreshed only by POST /reload or SIGHUP — not on every request.
Editing a policy file and expecting the very next write to see it without a
reload is the most likely surprise here.
Where next
Section titled “Where next”- Tutorial: restrict a pipeline with a security policy — a full walkthrough: write a policy, opt a pipeline in, watch a denied write get rejected.
- Restricting pipelines with security
policies —
the operator-facing guide: how to scope a policy, when to reach for one
vs. a deployment-wide
security.*toggle, and naming conventions that age well. - Configuration reference
—
security_policies_dirandsecurity_policies.requiredin full. - Gateway tool policy — the closest prior art, and the finer-grained boundary this feature deliberately doesn’t duplicate.
- Threat model — the one exception a
security policy carves out of the
admintoken’s otherwise-total write authority.