Skip to main content

pyintents

Declarative capability-based access control for Python functions.

Explore the docs »

Getting Started · Basic Usage · Latest Documentation · License


GitHub License GitHub forks GitHub commits since latest release GitHub Release Date GitHub Actions Workflow Status GitHub Actions Workflow Status PyPI - Downloads PyPI - Version GitHub contributors


📖 Overview

PyIntents brings capability-based security to Python.

Functions declare what they are allowed to do through an @intent decorator, and PyIntents enforces these permissions at call time. You define namespaces with allow and deny rules, recursively validate call chains, exempt trusted helpers when needed, and keep unknown or dynamic behavior blocked by default.

PyIntents is useful for:

  • plugin sandboxes;
  • AI agent tool control;
  • environment-specific security policies;
  • testing and dependency isolation;
  • security audits and explicit capability boundaries.

Trust explicitly. Fail safely.

PyIntents is not a full operating-system-level sandbox. It is a declarative policy layer for Python functions. For strong isolation of untrusted code, combine it with process isolation, containers, WASM, or a dedicated sandboxing solution.


🚀 Getting Started

Installation

pip install pyintents

Python 3.12+ is recommended.


Quick Example

from pyintents import IntentNamespace

# Allow only print()
namespace = IntentNamespace(uses=[print])


@namespace.intent()
def safe_function():
    print("This is allowed")  # OK


@namespace.intent()
def unsafe_function():
    import os
    os.system("echo bad")  # IntentViolationError

By default, PyIntents validates the function before execution.

If a forbidden or unknown call is found anywhere in the statically visible call chain, the decorated function is not executed.


🛡️ Basic Usage

1. Allow Specific Functions

from pyintents import IntentNamespace

namespace = IntentNamespace(uses=[print, len])


@namespace.intent()
def my_func():
    print("Hello")   # Allowed
    return len([1])  # Allowed

2. Recursive Enforcement

Recursive validation is enabled by default.

namespace = IntentNamespace(uses=[print])


def helper():
    print("Inside helper")


@namespace.intent(uses=[helper])
def main():
    helper()

PyIntents checks not only main, but also what helper calls.


3. Allow Functions From the Same Module

If your module has many internal helper functions, allowing each one manually can be tedious.

Use usemodule=True:

namespace = IntentNamespace(
    uses=[print],
    recursive=True,
    usemodule=True,
)


def inner():
    print("Inner")


def outer():
    print("Outer")
    inner()


@namespace.intent()
def func():
    outer()

With usemodule=True:

  • functions defined in the same module as the decorated function are allowed;
  • their calls are still recursively validated;
  • functions from other modules are not automatically allowed.

usemodule requires recursive=True.


4. Allow Local Nested Functions

uselocals=True allows functions defined inside the decorated function.

namespace = IntentNamespace(
    uses=[print],
    uselocals=True,
)


@namespace.intent()
def main():
    def local_helper():
        print("Local helper")

    local_helper()

Important:

  • uselocals=True allows nested local functions;
  • it does not automatically allow module-level global functions.

For module-level helpers, use usemodule=True or explicit uses=[...].


5. Exempt Trusted Functions From Allowlist Checks

without exempts a function from allowlist checks, but deny rules are still enforced.

def helper():
    print("OK")


namespace = IntentNamespace(
    uses=[print],
    without=[helper],
)


@namespace.intent()
def main():
    helper()

Important:

without does not mean “ignore everything inside this function forever”.

It means:

  • this function does not need to be explicitly allowed by uses;
  • but forbidden calls inside it can still be rejected.

6. Explicit Denial

import os

from pyintents import IntentNamespace

namespace = IntentNamespace(
    uses=[print],
    deny=[os.system],
)


@namespace.intent()
def restricted():
    print("OK")
    os.system("echo bad")  # Explicitly denied

You can also use string rules:

namespace = IntentNamespace(
    uses=[print],
    deny=["os.system"],
)

7. Runtime Layering

Decorator-level rules extend or override namespace-level rules.

base = IntentNamespace(uses=[print])


@base.intent(uses=[len])
def layered_func():
    print("Hi")
    return len("world")

8. Unknown and Dynamic Calls Are Blocked by Default

PyIntents is fail-closed by default.

Unknown calls are denied unless explicitly allowed or unless allow_unknown=True is set.

Dynamic primitives such as:

eval
exec
compile
__import__
getattr
setattr
delattr
globals
locals
vars
breakpoint

are denied by default.

You can disable this behavior with:

IntentNamespace(deny_dynamic_primitives=False)

but this is discouraged.


📦 API Reference

IntentNamespace

IntentNamespace(
    uses=None,
    *,
    recursive=True,
    without=None,
    uselocals=False,
    usemodule=False,
    deny=None,
    allow_unknown=False,
    deny_dynamic_primitives=True,
)
Parameter Type Default Description
uses Iterable[Callable or str] None Explicitly allowed functions or names
recursive bool True Recursively validate called functions
without Iterable[Callable or str] None Exempt from allowlist checks only
uselocals bool False Allow nested functions defined inside the decorated function
usemodule bool False Allow functions from the same module. Requires recursive=True
deny Iterable[Callable or str] None Explicitly forbidden functions or names
allow_unknown bool False Allow unresolved or opaque calls
deny_dynamic_primitives bool True Deny dynamic primitives like eval, exec, getattr, etc.

@namespace.intent()

Overrides or extends namespace settings per function.

@namespace.intent(
    uses=[print],
    recursive=True,
    without=[helper],
    uselocals=True,
    usemodule=True,
    deny=[os.system],
    allow_unknown=False,
)
def custom_func():
    pass

⚠️ Exceptions

IntentViolationError

Raised when a function violates declared permissions.

from pyintents import IntentNamespace, IntentViolationError

namespace = IntentNamespace(uses=[print])


@namespace.intent()
def bad():
    import os
    os.system("echo bad")


try:
    bad()
except IntentViolationError as exc:
    print(exc)

IntentParseError

Raised when function source code is unavailable or cannot be parsed.

from pyintents.exceptions import IntentParseError

IntentConfigurationError

Raised when namespace or decorator configuration is invalid.

from pyintents.exceptions import IntentConfigurationError

🔧 How It Works

PyIntents performs static policy validation before the decorated function is executed.

The pipeline is:

  1. AST Parsing PyIntents parses the function source code using Python's ast module.

  2. Call Graph Construction It builds a tree of statically visible function calls.

  3. Safe Resolution Call names are resolved to actual function objects when possible.

  4. Rule Matching Each call is validated against:

    • uses;
    • deny;
    • without;
    • uselocals;
    • usemodule;
    • unknown-call policy;
    • dynamic-primitive policy.
  5. Pre-Execution Enforcement If a violation is found, the decorated function is not executed.

Example:

func -> outer -> inner -> os.system

If os.system is forbidden or unknown, PyIntents blocks the root call to func before any code inside func runs.


🧠 Security Model

PyIntents follows a fail-closed model.

By default:

  • only explicitly allowed calls are permitted;
  • recursive validation is enabled;
  • unknown calls are denied;
  • dynamic primitives are denied;
  • functions without available source code are treated carefully;
  • violations prevent execution.

Pre-Execution Validation

PyIntents validates the call chain before execution.

This means:

@namespace.intent()
def func():
    print("Func")
    outer()

If outer eventually calls something forbidden, func will not execute at all.

This is intentional.

It prevents partially executed functions from producing side effects before a violation is detected.


⚠️ Limitations

PyIntents is a static and runtime policy layer, not a complete sandbox.

Python is highly dynamic, so some behavior cannot be fully analyzed statically.

Examples:

getattr(os, "system")("echo bad")
eval("os.system('echo bad')")
globals()["os"].system("echo bad")

PyIntents mitigates many of these cases by denying dynamic primitives by default, but no AST-only solution can guarantee complete isolation.

For strong security boundaries, use:

  • subprocesses;
  • containers;
  • seccomp;
  • WASM;
  • RestrictedPython;
  • custom import hooks;
  • runtime monitoring.

🎯 Use Cases

Use Case Description
Plugin Sandboxes Restrict what third-party plugins can do
AI Agent Control Limit tool access for LLM agents
Environment Policies Enforce different rules per environment
Testing Isolate unit tests from external dependencies
Security Audits Document and enforce capability boundaries
Internal APIs Prevent accidental access to dangerous helpers

📚 Documentation


📄 License

Licensed under the GNU General Public License v3.0.

See LICENSE for details.


🤝 Contributing

Contributions are welcome.

Feel free to:

  • open issues;
  • submit pull requests;
  • suggest features;
  • improve documentation;
  • report security concerns.

🌟 Support

If you find PyIntents useful, consider:

  • ⭐ starring the repository on GitHub;
  • 🐛 reporting issues;
  • 💡 suggesting features;
  • 📖 improving documentation.

Trust explicitly. Fail safely.

↑ Back to top

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyintents-0.2.0.tar.gz (11.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyintents-0.2.0-py3-none-any.whl (13.3 kB view details)

Uploaded Python 3

File details

Details for the file pyintents-0.2.0.tar.gz.

File metadata

  • Download URL: pyintents-0.2.0.tar.gz
  • Upload date:
  • Size: 11.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pyintents-0.2.0.tar.gz
Algorithm Hash digest
SHA256 026bfe26ad345e330a87b5a1f13242d04d9fa7c9e5bdd9de3c4ef866139f61fa
MD5 bb93f2d30bb495018adabbc66b000e64
BLAKE2b-256 ce854723d41760a83e0c5061b4642dc754a6b82b472b5432c57418c8c1d231a2

See more details on using hashes here.

File details

Details for the file pyintents-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pyintents-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pyintents-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1a7bb4efae40a393b8b2632d01fe77e672495f5631c4fe38c0c9a047f42bcaa4
MD5 fc30ebf302496eeb14b0a297c769642f
BLAKE2b-256 fea8ad31f14acc8761e197a59c7f25fe6972e3446529c97e67fb6e96bb24ed2a

See more details on using hashes here.

Release history Release notifications | RSS feed

0.3.0

2 files

This release

0.2.0 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page