Synaptic Security Guide

This guide explains the security capabilities built into synaptic in practical terms.

The important mental model is simple:

Alpha.11 Security Posture

0.3.0-alpha.11 hardens the security branch for alpha production trials:

These controls are defense-in-depth, not a compliance certification. Prompt injection and sensitive-data detection are heuristic and must be combined with least-privilege tools, approvals, tenant isolation, provider controls, and application-specific policy.

Quick Start

See the built-in posture profiles:

Synaptic.security_profiles()

Inspect the effective security posture without running anything:

Synaptic.explain_security(:chat, security_profile: :high_assurance)
Synaptic.explain_security(:workflow, security_profile: :regulated)

Run a chat call with a coherent bundled posture:

Synaptic.Tools.chat(messages,
  security_profile: :high_assurance
)

Attach a caller-visible explanation of what was active:

{:ok, content, %{security: report}} =
  Synaptic.Tools.chat(messages,
    security_profile: :production,
    security_explain: true
  )

Turn posture/config mistakes into hard failures:

Synaptic.Tools.chat(messages,
  security_profile: :regulated,
  security_diagnostics: [on_error: :error]
)

Explain a blocked action or guardrail failure:

case Synaptic.Tools.chat(messages, security_profile: :high_assurance) do
  {:error, reason} ->
    Synaptic.explain_error(reason)

  other ->
    other
end

Capability Index

Security posture profiles

What it does:

You may use this when:

Ideal when:

Validation boundary

What it does:

You may use this when:

Ideal when:

Sanitization boundary

What it does:

You may use this when:

Ideal when:

Prompt security boundary

What it does:

You may use this when:

Ideal when:

Privacy / PII boundary

What it does:

You may use this when:

Ideal when:

Tool policy and approvals

What it does:

You may use this when:

Ideal when:

Action controls

What it does:

You may use this when:

Ideal when:

Central security policy

What it does:

You may use this when:

Ideal when:

Policy hooks and MCP governance

What it does:

You may use this when:

Ideal when:

Context hygiene

What it does:

You may use this when:

Ideal when:

Factuality and output controls

What it does:

You may use this when:

Ideal when:

Audit and telemetry

What it does:

You may use this when:

Ideal when:

Runtime security

What it does:

You may use this when:

Ideal when:

Outbound egress control

What it does:

You may use this when:

Ideal when:

Connector / MCP gateway

What it does:

You may use this when:

Ideal when:

Troubleshooting surface

What it does:

You may use this when:

Ideal when:

If you want a low-friction starting point:

config :synaptic, Synaptic.Security,
  profile: :production,
  diagnostics: [
    enabled: true,
    on_error: :warn,
    emit_warnings: true,
    emit_on_startup: true
  ]

If you want a stricter default:

config :synaptic, Synaptic.Security,
  profile: :high_assurance,
  diagnostics: [
    enabled: true,
    on_error: :warn,
    emit_warnings: true,
    emit_on_startup: true
  ]

If you want full explicit control and no bundled posture:

Synaptic.Tools.chat(messages, security_profile: false)

Detailed Capabilities

Security Posture Profiles

Use this when you want synaptic to feel opinionated instead of bare.

Built-in profiles:

Inspect them:

Synaptic.security_profiles()
Synaptic.explain_security(:chat, security_profile: :high_assurance)

Apply them:

Synaptic.Tools.chat(messages,
  security_profile: :high_assurance
)

Synaptic.start(MyWorkflow, input,
  security_profile: :regulated
)

Practical note:

Validation Boundary

Use this when incorrect shape should stop execution early.

Workflow example:

defmodule SignupWorkflow do
  use Synaptic.Workflow

  step :collect,
    input: %{
      email: [type: :string, regex: ~r/^[^\s]+@[^\s]+\.[^\s]+$/, required: true],
      plan: [type: :string, enum: ["free", "pro"], required: true]
    },
    validation: [input: :strict, output: :strict] do
    {:ok, %{accepted: true}}
  end

  commit()
end

Tool validation example:

Synaptic.Tools.chat(messages,
  validation: [tools: true],
  tools: [
    %{
      name: "search_docs",
      description: "Search internal docs",
      schema: %{
        type: "object",
        properties: %{
          query: %{type: "string", minLength: 3}
        },
        required: ["query"]
      },
      handler: fn %{"query" => query} -> {:ok, %{results: [query]}} end
    }
  ]
)

You may want this especially for:

Sanitization Boundary

Use this when untrusted text should be normalized before it reaches prompts or tools.

Example:

Synaptic.Tools.chat(messages,
  sanitization: [
    enabled: true,
    prompt: [enabled: true, roles: ["user"], types: [:html, :markdown]],
    tools: [enabled: true],
    tool_results: [enabled: true]
  ]
)

Practical effect:

Ideal when:

Prompt Security Boundary

Use this when the model sees untrusted content and can do things with it.

Example:

Synaptic.Tools.chat(messages,
  prompt_security: [
    enabled: true,
    response: [on_detection: :error]
  ]
)

Practical effect:

Ideal when:

Privacy / PII Boundary

Use this when the model should reason over sensitive input without seeing raw identifiers.

Example:

Synaptic.Tools.chat(messages,
  privacy: [
    enabled: true,
    prompt: [
      enabled: true,
      default_action: :tokenize,
      derived_facts: true
    ],
    output: [
      enabled: true,
      default_action: :mask,
      rehydrate: false
    ]
  ]
)

Practical effect:

Ideal when:

Tool Policy and Approvals

Use this when some tools are harmless and others are risky.

Tool definition example:

tool = %Synaptic.Tools.Tool{
  name: "delete_invoice",
  description: "Delete one invoice by id",
  schema: %{
    type: "object",
    properties: %{invoice_id: %{type: "string"}},
    required: ["invoice_id"]
  },
  handler: fn %{"invoice_id" => id} -> {:ok, %{deleted: id}} end,
  destructive: true,
  needs_approval: true,
  risk: :high,
  data_class: :customer_data
}

Policy example:

Synaptic.Tools.chat(messages,
  tools: [tool],
  policy: [
    enabled: true,
    require_approval: [destructive: true, risk_at_or_above: :high],
    return_decisions: true
  ]
)

Ideal when:

Action Controls

Use this when allowed actions still need runtime guardrails.

Example:

Synaptic.Tools.chat(messages,
  action_controls: [
    enabled: true,
    idempotency: [
      enabled: true,
      require_for: [destructive: true]
    ],
    retries: [
      enabled: true,
      max_attempts: 2,
      sources: [:mcp]
    ]
  ]
)

Practical effect:

Central Security Policy

Use this when prompt and tool policy should share one tenant-aware or sensitivity-aware rule layer.

Example:

Synaptic.Tools.chat(messages,
  tenant: "acme",
  security_policy: [
    enabled: true,
    tenant: [required_surfaces: [:prompt, :tool]],
    pii: [
      model_export: [
        enabled: true,
        sensitivity_at_or_above: :restricted_pii,
        allow_models: ["gpt-4o-mini"]
      ]
    ]
  ]
)

Ideal when:

Hooks and MCP Governance

Use this when a platform team wants central policy logic without patching core runtime code.

Example:

Synaptic.Tools.chat(messages,
  hooks: [
    enabled: true,
    pre_prompt: [
      fn payload ->
        {:ok, update_in(payload.messages, fn msgs ->
          [%{role: "system", content: "Company prompt preamble"} | msgs]
        end)}
      end
    ]
  ],
  mcp: [:docs],
  mcp_governance: [
    enabled: true,
    managed_only: true
  ]
)

Ideal when:

Context Hygiene

Use this when prompts get large or retain too much raw tool output.

Example:

Synaptic.Tools.chat(messages,
  context_hygiene: [
    enabled: true,
    tool_results: [
      spill_oversized: true,
      compact_older: true,
      keep_recent: 2,
      max_inline_chars: 2_000
    ]
  ]
)

Fetch a spilled result later:

Synaptic.fetch_spilled_tool_result(handle)

Ideal when:

Factuality and Output Controls

Use this when the final answer needs stronger release-time checks.

Example:

Synaptic.Tools.chat(messages,
  factuality: [
    enabled: true,
    checks: [
      require_provenance: true,
      detect_unsupported_claims: true
    ],
    response: [on_violation: :abstain]
  ]
)

With a custom verifier:

Synaptic.Tools.chat(messages,
  factuality: [
    enabled: true,
    verification: [
      enabled: true,
      verifier: fn payload ->
        if String.contains?(payload.content, "verified") do
          :ok
        else
          {:error, %{code: :custom_verifier_failed, message: "Missing verifier marker"}}
        end
      end
    ]
  ]
)

Ideal when:

Audit and Telemetry

Use this when you need sanitized operational visibility.

Example:

Synaptic.Tools.chat(messages,
  audit: [
    enabled: true,
    return_metadata: true
  ]
)

Read audit records:

Synaptic.audit_records(run_id)
Synaptic.verify_audit_records(run_id)

Delete run artifacts:

Synaptic.delete_run_artifacts(run_id)

Ideal when:

Runtime Security

Use this when workflow state itself should be minimized or redacted.

Example:

Synaptic.start(MyWorkflow, input,
  runtime_security: [
    enabled: true,
    history: [redact: true, max_entries: 10],
    events: [redact: true],
    snapshot: [redact: true],
    retention: [purge_context_on_terminal: true]
  ]
)

Ideal when:

Outbound Egress and Connector Gateway

Use these together when remote network access should be intentional and auditable.

Strict OpenAI-only egress:

config :synaptic, Synaptic.EgressPolicy,
  enabled: true,
  openai: [allow_hosts: ["api.openai.com"]],
  mcp: [allow_hosts: []],
  voice: [
    allow_hosts: [
      "api.openai.com",
      "api.elevenlabs.io",
      "generativelanguage.googleapis.com"
    ]
  ]

Controlled local MCP development example:

Synaptic.Tools.chat(messages,
  mcp: [
    %{
      name: "docs",
      transport: :http,
      adapter: Synaptic.MCP.Adapters.HTTP,
      base_url: "http://localhost:4001/mcp"
    }
  ],
  egress: [
    enabled: true,
    mcp: [
      allow_hosts: ["localhost"],
      allow_localhost: true,
      allow_schemes: ["http"]
    ]
  ],
  connector_gateway: [
    enabled: true,
    managed_only: false,
    auth: [forbid_passthrough_headers: true],
    session_binding: [enabled: true, require_run_id: true]
  ],
  run_id: "dev-mcp-session"
)

Practical effect:

Ideal when:

Troubleshooting

Use this whenever a call fails and the reason is not obvious.

Examples:

Synaptic.explain_error({:egress_blocked, detail})
Synaptic.explain_error({:connector_gateway_blocked, detail})
Synaptic.explain_error({:validation_failed, details})

Practical workflow:

  1. capture the returned error
  2. run Synaptic.explain_error/1
  3. run Synaptic.explain_security/2 with the same opts
  4. only then decide whether to relax policy or fix input

Common Recipes

Protect PII but still allow tool execution

Synaptic.Tools.chat(messages,
  security_profile: :high_assurance,
  privacy: [
    enabled: true,
    prompt: [default_action: :tokenize, derived_facts: true],
    output: [default_action: :mask, rehydrate: false]
  ]
)

Use when:

Run tools safely

Synaptic.Tools.chat(messages,
  security_profile: :high_assurance,
  validation: [tools: true],
  policy: [
    enabled: true,
    require_approval: [destructive: true, risk_at_or_above: :high]
  ],
  action_controls: [
    enabled: true,
    idempotency: [enabled: true, require_for: [destructive: true]]
  ]
)

Use when:

Enable MCP securely

Synaptic.Tools.chat(messages,
  mcp: [:docs],
  egress: [
    enabled: true,
    mcp: [allow_hosts: ["docs.example.com"]]
  ],
  connector_gateway: [
    enabled: true,
    managed_only: true,
    tls: [require_https: true],
    session_binding: [enabled: true]
  ],
  mcp_governance: [
    enabled: true,
    managed_only: true
  ]
)

Use when:

Lock down network access

Synaptic.Tools.chat(messages,
  security_profile: :high_assurance,
  egress: [
    openai: [allow_hosts: ["api.openai.com"]],
    mcp: [allow_hosts: []]
  ],
  connector_gateway: [
    enabled: true,
    tls: [require_https: true],
    auth: [forbid_passthrough_headers: true]
  ]
)

Use when:

Triage a denied action

case Synaptic.Tools.chat(messages, security_profile: :regulated) do
  {:error, reason} ->
    %{
      explanation: Synaptic.explain_error(reason),
      posture: Synaptic.explain_security(:chat, security_profile: :regulated)
    }

  ok ->
    ok
end

Use when:

Defaults and Practical Notes

Security Boundaries and Known Limits

If you want the shortest practical path:

  1. start with security_profile: :production
  2. move to :high_assurance when tools or MCP become important
  3. use :regulated when tenant-aware controls and answer verification matter
  4. keep Synaptic.explain_security/2 and Synaptic.explain_error/1 in your normal debugging workflow