Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,17 @@ ADMIN_API_KEY=
# by default. Single-tenant self-hosted operators who intentionally use code
# steps can opt in; leave false on multi-tenant deployments.
CODE_STEPS_ALLOW_UNTRUSTED=false
# Run "code" steps in a separate Python subprocess (out-of-process isolation) so
# a sandbox escape cannot reach the parent process memory (settings, DB session,
# other tenants' data). On by default; set false to use the legacy in-process path.
CODE_STEPS_OUT_OF_PROCESS=true
# Bearer token for the Memory MCP server over streamable-http. Required to start
# streamable-http outside local mode (it fails closed without it). stdio is
# unaffected. Callers must send: Authorization: Bearer <token>
# MEMORY_MCP_TOKEN=
# Optional tenant scope prefix for the Memory MCP server. When set, every tool
# call (add/search/forget/list_memories) must operate within this prefix, so an
# authenticated token cannot read/write/delete another tenant's memories. Leave
# unset for single-tenant/local deployments (no cross-tenant enforcement).
# MEMORY_MCP_SCOPE_PREFIX=
LOG_LEVEL=info
4 changes: 3 additions & 1 deletion .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ jobs:
python:
name: Python tests
runs-on: ubuntu-latest
timeout-minutes: 15
# ~17k tests run in ~13.5 min plus dashboard build and dependency install;
# 20 min leaves headroom over the total setup + run time.
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.40.3] - 2026-07-11 - "Deeper Isolation"

Follow-up hardening that finishes the three deferred items from the 0.40.2 audit sweep. Defaults
are secure; existing behavior is preserved with per-feature opt-outs.

### Security
- **Code steps now run out-of-process by default.** Validated `code` steps execute in a separate
Python subprocess with a secret-free environment and POSIX CPU/address-space limits, so a
sandbox escape cannot reach the parent process memory (`settings`, DB session factory, other
tenants' data). The subprocess is really killed on timeout (the in-process thread path could
not be). Set `CODE_STEPS_OUT_OF_PROCESS=false` to use the legacy in-process path; infrastructure
failures fall back to it automatically so a step is never lost.
- **HTTP steps pin the validated IP at connect time.** The SSRF pre-flight resolves the hostname
once, validates the address, and a custom transport dials that exact IP while preserving the
`Host` header and TLS SNI, closing the DNS-rebind TOCTOU. Non-resolvable/mocked hosts are
unaffected.
- **Memory MCP per-tenant scope enforcement.** With `MEMORY_MCP_SCOPE_PREFIX` set, every tool call
(`add`/`search`/`list_memories`/`forget`) must stay within the configured scope, so an
authenticated caller cannot reach another tenant's memories by supplying a different scope
string. `forget` requires and validates the owning `user_id` against the prefix. Unset preserves
today's single-tenant behavior.

## [0.40.2] - 2026-07-11 - "Hardened Steps"

Security and correctness patch from a full audit sweep (engine, API, CLI, sandbox, deps). No new
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Build once. Run anywhere.** Sandcastle is an open-source, production-ready orchestrator for AI agents. **Describe a workflow in plain English and it builds it** (or write the YAML yourself); run it on **any model** — Claude, GPT, Mistral, or a local model on your own box — and move between them with one line; deploy it **your way** — cloud, your own server, fully air-gapped, or EU-only; and it **gets better over time**, on its own. Local models run at `$0/run` with hard data-residency enforcement and a tamper-evident audit trail; the cloud is there too, with 7 providers and auto-failover, 22 step types, verified templates, and a full dashboard. Sovereign by default. European-built.

[![PyPI](https://img.shields.io/badge/PyPI-v0.40.2-blue?style=flat-square)](https://pypi.org/project/sandcastle-ai/0.40.2/)
[![PyPI](https://img.shields.io/badge/PyPI-v0.40.3-blue?style=flat-square)](https://pypi.org/project/sandcastle-ai/0.40.3/)
[![License: BSL 1.1](https://img.shields.io/badge/License-BSL_1.1-blue.svg)](LICENSE)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![Tests](https://img.shields.io/badge/tests-17900%2B%20passing-brightgreen?style=flat-square)](https://github.com/gizmax/Sandcastle/actions)
Expand Down Expand Up @@ -722,7 +722,9 @@ All backends share the same `SandboxBackend` protocol - same YAML, same API, sam
| Setting | Default | Meaning |
|---|---|---|
| `CODE_STEPS_ALLOW_UNTRUSTED` | `false` | In-process `code` steps are admin-only by default. Set `true` on single-tenant self-hosted deployments to let any authenticated caller run them; keep `false` for multi-tenant. |
| `CODE_STEPS_OUT_OF_PROCESS` | `true` | Run `code` steps in a separate Python subprocess so a sandbox escape cannot reach the parent process memory (settings, DB session, other tenants' data). The child is really killed on timeout. Set `false` to fall back to the legacy in-process path. |
| `MEMORY_MCP_TOKEN` | — | Bearer token for the Memory MCP `streamable-http` transport. Required to start it outside local mode (fails closed); callers must send `Authorization: Bearer <token>`. stdio is unaffected. |
| `MEMORY_MCP_SCOPE_PREFIX` | — | Optional tenant scope prefix for the Memory MCP server. When set, every tool call (`add`/`search`/`forget`/`list_memories`) must operate within this prefix, so an authenticated token cannot cross tenants. Unset preserves single-tenant behavior. |

---

Expand Down
2 changes: 1 addition & 1 deletion deploy/mcp-tunnel/memory-mcp/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: memory-mcp
description: Sandcastle Memory MCP server behind a Cloudflare MCP tunnel (outbound-only, no inbound ingress required)
type: application
version: 0.1.0
appVersion: "0.40.2"
appVersion: "0.40.3"
keywords:
- sandcastle
- mcp
Expand Down
2 changes: 1 addition & 1 deletion src/sandcastle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Sandcastle - Production-ready workflow orchestrator for AI agents."""

__version__ = "0.40.2"
__version__ = "0.40.3"

__all__ = ["SandcastleClient", "AsyncSandcastleClient", "__version__"]

Expand Down
7 changes: 7 additions & 0 deletions src/sandcastle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ class Settings(BaseSettings):
# intentionally use code steps can opt in via CODE_STEPS_ALLOW_UNTRUSTED=true.
code_steps_allow_untrusted: bool = False

# Run "code" steps in a separate Python subprocess (out-of-process isolation)
# so a sandbox escape cannot reach the parent process memory (settings, DB
# session factory, other tenants' data). On by default. Operators can fall
# back to the legacy in-process path via CODE_STEPS_OUT_OF_PROCESS=false if
# the subprocess path misbehaves in their environment.
code_steps_out_of_process: bool = True

# Database (empty = local SQLite mode)
database_url: str = ""

Expand Down
Loading
Loading