Security

Security model

Plugins run as Python code with the same privileges as Flowly itself. There's no sandbox. This page is what plugin authors and users need to know to keep this honest tradeoff safe.

Threat model

Flowly's plugin runtime is intentionally trust-based. A plugin you load can:

  • Read and write any file the Flowly process can
  • Run arbitrary shell commands
  • Make network requests to anywhere
  • Read environment variables (including API keys)
  • Read other plugins' state files at $FLOWLY_HOME/<other-plugin>/

This is the same privilege level as a pip package or npm dependency. Treat plugin authors as you would any third-party dependency author — read the code or trust the source.

There is no sandbox
Flowly does not isolate plugins from each other or from the host system. Loading a malicious plugin is equivalent to running its Python source directly. Only install plugins from sources you trust.

For plugin authors

The bargain Flowly offers: we don't sandbox you, but we expect you to be honest about what your plugin does and conservative about what it touches.

  • Declare everything in your manifest. provides_tools, provides_hooks, requires_env — these are the user's first line of trust. List every register call honestly.
  • Document side effects in your README. Network destinations, files written, env vars consumed. Users decide based on this.
  • Default to scope-tight. Don't scan the entire home directory if you only need $FLOWLY_HOME. Don't hit the network at register time. Don't read any file the user didn't explicitly point you at.
  • No surprises in updates. If v2 of your plugin starts doing something v1 didn't, document it loudly. Adding network access in a minor version is hostile.

For plugin users

When evaluating a plugin before installing:

  1. Read the manifest. cat ~/.flowly/plugins/<name>/plugin.yaml (after install) or fetch it from the source repo. Look at provides_tools and provides_hooks.
  2. Skim the source. Plugin code is plain Python. Even a 30-second skim catches obvious red flags — outbound HTTP to unknown hosts, reading ~/.ssh/, eval()ing user input.
  3. Prefer pinned versions. flowly plugins install owner/repo tracks the default branch. For production, pin to a specific commit or tag once you have a Git URL.
  4. Audit before enabling. install copies files; the plugin only runs after enable + gateway restart. Use the gap to inspect.
Bundled plugins
The plugins shipped with Flowly itself — disk-cleanup, auto-commit — are reviewed before each release and follow these guidelines. They're a reasonable baseline of "safe".

Handling secrets

Many plugins need API keys or OAuth tokens. Some patterns to follow, some to avoid.

Do

  • Read from env vars or a local credentials file under your plugin's state dir, mode 0600:
    python
    creds_file = _get_state_dir() / "credentials.json"
    creds_file.parent.mkdir(parents=True, exist_ok=True)
    creds_file.write_text(json.dumps(creds))
    creds_file.chmod(0o600)
  • Document required env vars in the manifest via requires_env:
    yaml
    requires_env:
      - name: ACME_API_KEY
        description: "API key from acme.example.com/dashboard"
        secret: true
  • Refresh tokens lazily. Detect expiration on the next API call and refresh; don't poll.

Don't

  • Hardcode keys in your source — even "just for testing"
  • Log credentials, even partially. log.warning(f"auth failed: {token[:6]}...") ends up in audit files
  • Send credentials to anywhere except the intended service. No telemetry, no "help us debug" phone-home
  • Read other plugins' credentials.json files. Stay in your own state directory

Honest manifest declaration

The manifest is the user's primary trust signal. Be exhaustive:

yaml
name: my-crm-plugin
version: 1.2.0
description: "Look up accounts, log activities, and create tasks in our internal CRM."
author: "platform@acme.com"
kind: standalone

provides_tools:
  - crm_lookup_account
  - crm_log_activity
  - crm_create_task

provides_hooks:
  - post_tool_call           # writes audit log of CRM-touching tool calls

requires_env:
  - name: ACME_CRM_TOKEN
    description: "Bearer token from CRM dashboard → Settings → API"
    secret: true
  - name: ACME_CRM_BASE_URL
    description: "Your CRM instance URL (e.g. https://acme.crm.example.com)"
    secret: false

A user reading this knows: three tools, one observability hook, two env vars (one secret), only network egress is to the CRM URL they provided. That's the trust contract.

Scope your filesystem and network

Operate on the smallest set of paths and hosts your feature requires. Some practical rules:

  • Never write outside your state dir or a user-provided path. If your plugin tracks files, store the index in your own $FLOWLY_HOME/<plugin>/, not the file's parent dir.
  • Allow-list, don't deny-list. Tell users which paths your plugin operates on (auto-commit requires explicit /add calls). Don't scan home and skip "dangerous" subdirectories — that's a footgun.
  • Network egress to known hosts only. Hardcode the base URL of the service you integrate with, or read it from a user-provided env var. Don't accept arbitrary URLs.
  • Timeouts always. httpx.AsyncClient(timeout=10). A hung request blocks the agent loop.

Errors must not leak

Two failure modes to avoid: leaking secrets in error messages, and breaking the agent loop with unhandled exceptions.

python
# ❌ Bad — token may end up in an audit log seen by a different operator
try:
    api.call(token=user_token)
except Exception as exc:
    return f"API call failed: {exc}"   # may include token in repr

# ✅ Good — generic message, full detail in your own log file with proper ACL
try:
    api.call(token=user_token)
except Exception as exc:
    _log({"event": "api_failed", "error": type(exc).__name__})
    return "Failed to reach CRM. Check your token and try again."

And on the loop side: every plugin entry point should swallow its exceptions:

python
def on_post_tool_call(ctx):
    try:
        # ... your hook logic
    except Exception as exc:
        _log({"event": "hook_error", "error": str(exc)})
        # Return None — never raise. The agent must keep running.
The Flowly invariant
A plugin failure should disable that plugin (logged, audited), but must not affect the agent loop, other plugins, or the user's conversation. If your code can raise into the runtime, you have a bug.