This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/STARLARK-REFERENCE.md
Jeffrey Smith 3c403dd884
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m53s
CI/CD / test-sqlite (push) Successful in 2m56s
CI/CD / build-and-deploy (push) Successful in 1m18s
Feat v0.8.4 docs surface fix (#71)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-03 10:43:13 +00:00

426 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Starlark Reference
Armature extensions can include Starlark scripts for server-side logic.
Starlark is a Python-like language designed for configuration and
embedding — see the [official spec](https://github.com/google/starlark-go).
## Sandbox constraints
- **No `while` loops** — use `for` with bounded ranges.
- **No `load()`** — use `lib.require()` for library dependencies.
- **Max steps:** 1,000,000 bytecode operations per execution.
- **No filesystem or OS access** — all I/O goes through gated modules.
- **Deterministic** — same input produces same output (no `random`, no `time`).
## Always-available modules
These modules are injected into every script with no permission required.
### json
Standard Starlark JSON module.
```python
data = json.decode('{"key": "value"}')
text = json.encode({"key": "value"})
```
### settings
Read resolved package settings (global → team → user cascade).
```python
val = settings.get("theme", "light")
# Returns the resolved value, or the default if unset.
```
The cascade respects the `user_overridable` flag from the package manifest.
See [Permissions & Groups](PERMISSIONS-AND-GROUPS) for details.
```python
# Check if a runtime capability is available
if settings.has_capability("pgvector"):
# Use native vector search
...
```
`has_capability(name)` returns `True` if the named environment capability
is detected by the kernel. Detected capabilities: `pgvector`, `workspace`,
`object_storage`, `s3`, `postgres`.
### lib
Load exported functions from library packages.
```python
helpers = lib.require("my-utils")
result = helpers.format_date("2026-01-15")
```
Requirements:
- The library must be declared in your package manifest's `dependencies`.
- The library must be type `library`, status `active`, tier `starlark`.
- Circular dependencies are detected and rejected.
- Results are cached per execution (calling `require` twice returns the
same object).
### permissions
Check whether a user has a specific permission.
```python
if permissions.check(user_id, "image-gen.use"):
# User is authorized
...
```
Returns `True` if the user has the permission, `False` otherwise (including
when the user is not found). Resolves the user's groups and merges granted
permissions — works for both kernel and extension-declared permissions.
## Permission-gated modules
These modules are only available if the package has the corresponding
permission granted in **Admin > Packages**.
### secrets
**Permission:** `secrets.read`
Read admin-configured secrets for this package.
```python
api_key = secrets.get("OPENAI_KEY") # str or None
all_keys = secrets.list() # list of key names
```
Secrets are set in **Admin > Packages > Secrets** and scoped per package.
### notifications
**Permission:** `notifications.send`
Send in-app notifications to users.
```python
notifications.send(
user_id, # str — target user UUID
title, # str — notification title
body="", # str — optional body text
type="extension.notify" # str — notification type
)
```
### db
**Permission:** `db.read` (queries) or `db.write` (mutations)
Read and write extension data tables. All tables are automatically
namespaced as `ext_{package_id}_{table_name}`.
#### Read operations
```python
# Query with filters, ordering, and pagination
rows = db.query(
"tasks", # table name (without prefix)
filters={"status": "open"}, # equality WHERE clauses
order="-created_at", # column name (prefix - for DESC)
limit=50, # max 1000
before={"created_at": ts}, # range: column < value
after={"created_at": ts}, # range: column > value
search_like={"title": "%bug%"} # LIKE/ILIKE search
)
# Read from system views (read-only)
users = db.view("users", filters={"display_name": "Alice"}, limit=10)
channels = db.view("channels", limit=100)
# List all tables owned by this package
tables = db.list_tables()
```
Available views: `users`, `channels`.
#### Aggregate operations
```python
# Count rows matching filters
count = db.count("tasks", filters={"status": "open"})
# Aggregate a column (sum, avg, min, max, count)
total = db.aggregate("orders", "amount", "sum", filters={"status": "paid"})
# Returns int, float, or None (if no matching rows)
# Batch multiple queries in a single call
results = db.query_batch([
{"table": "tasks", "filters": {"status": "open"}, "limit": 10},
{"table": "logs", "order": "-created_at", "limit": 5},
])
# Returns list of result lists. Max 10 queries per batch.
# Each query spec supports: table (required), filters, order, limit, before, after, search_like
```
#### Vector similarity search
```python
# Find rows with the most similar embeddings (cosine distance)
rows = db.query_similar(
"documents", # table name
"embedding", # vector column name
vector=[0.1, 0.2, ...], # query vector (list of floats)
limit=10, # max results (default 10, max 100)
filters={"active": True}, # optional equality filters
metric="cosine", # only "cosine" supported
)
# Returns rows ordered by ascending _distance (0.0 = identical, 1.0 = orthogonal)
# Each row dict includes an injected "_distance" float key.
```
Vector columns are declared as `"vector(N)"` in the manifest `db_tables` block
(N = dimension, 14096). Storage varies by backend:
| Backend | Column type | Search |
|---------|-------------|--------|
| Postgres + pgvector | `vector(N)` with HNSW index | Native `<=>` operator |
| Postgres (no pgvector) | `JSONB` | Go-side cosine computation |
| SQLite | `TEXT` | Go-side cosine computation |
Insert vectors as lists: `db.insert("docs", {"embedding": [0.1, 0.2, 0.3]})`.
#### Write operations
```python
row = db.insert("tasks", {"title": "Fix bug", "status": "open"})
# Returns the inserted row dict (with generated id, created_at)
db.update("tasks", row_id, {"status": "closed"})
# Returns True on success
db.delete("tasks", row_id)
# Returns True on success
```
### http
**Permission:** `api.http`
Make outbound HTTP requests.
```python
resp = http.get("https://api.example.com/data", headers={"Authorization": "Bearer ..."})
resp = http.post(url, body='{"key": "val"}', headers={"Content-Type": "application/json"})
resp = http.put(url, body="...", headers={})
resp = http.delete(url, headers={})
resp = http.request("PATCH", url, body="...", headers={})
```
Response dict:
```python
{
"status": 200,
"headers": {"content-type": "application/json"},
"body": "..." # capped at 1 MB
}
```
#### Batch requests
```python
responses = http.batch([
{"method": "GET", "url": "https://api.example.com/a"},
{"method": "POST", "url": "https://api.example.com/b", "body": "{}", "headers": {"Content-Type": "application/json"}},
])
# Returns list of response dicts (same shape as individual calls).
# Individual failures return {"status": 0, "body": "error: ...", "headers": {}}.
# Max 10 requests per batch. Dispatched concurrently.
```
**Security:**
- Private/loopback IPs are blocked (SSRF protection).
- Packages can declare `network_access.allow` (allowlist) or
`network_access.block` (blocklist) in their manifest.
- Max 10 redirects. 10-second timeout. 1 MB response body limit.
### realtime
**Permission:** `realtime.publish`
Publish WebSocket events to subscribed clients.
```python
realtime.publish(
"my-channel", # channel name
"item.updated", # event label
{"id": "abc"} # payload dict (max 7 KB)
)
```
The payload is automatically tagged with `_pkg: package_id`.
### connections
**Permission:** `connections.read`
Read external connection configurations (secrets are decrypted).
```python
conn = connections.get("postgres", "main-db")
# Returns dict with id, type, name, scope, plus flattened config fields
# Returns None if not found
all_pg = connections.list("postgres")
# Returns list of connection dicts
```
Connections are resolved via scope chain: personal → team → global.
### workflow
**Permission:** `workflow.access`
Read workflow definitions and instances (read-only from Starlark;
mutations go through the HTTP API).
```python
defn = workflow.get_definition(workflow_id)
# Returns dict: id, name, slug, entry_mode, is_active, version, stages[]
inst = workflow.get_instance(instance_id)
# Returns dict: id, workflow_id, current_stage, status, stage_data, ...
instances = workflow.list_instances(workflow_id, status="active")
# Returns list of instance dicts
```
### batch
**Permission:** `batch.exec`
Run multiple callables concurrently. Each callable gets its own
execution thread with an independent step budget.
```python
jira = lib.require("jira-client")
confluence = lib.require("confluence-client")
results, errors = batch.exec([
lambda: jira.create_issue(issue_data),
lambda: confluence.create_page(page_data),
lambda: send_notification(user_id),
], timeout=15)
# results[i] = return value of callables[i], or None on error
# errors[i] = None on success, or error string on failure
# All three ran concurrently.
```
**Constraints:**
- Max 8 callables per call. Dispatched concurrently via goroutines.
- `timeout` (optional): 130 seconds per branch (default 10).
- `lib.require()` is not available inside branch callables.
Load libraries before the `batch.exec` call.
- `batch.exec()` cannot be called from within a branch (no nesting).
- `print()` output from branches is discarded.
---
### files
**Permissions:** `files.read`, `files.write`
Store and retrieve files via the kernel ObjectStore (PVC or S3).
All keys are scoped to `ext/{packageID}/` — extensions cannot access
each other's files.
```python
# Store a file with optional metadata
files.put("reports/q1.pdf", pdf_bytes,
content_type="application/pdf",
metadata={"quarter": "Q1", "year": 2026})
# Read a file
result = files.get("reports/q1.pdf")
# result = {"content": b"...", "content_type": "application/pdf",
# "size": 12345, "metadata": {"quarter": "Q1", "year": 2026}}
# Metadata only (no content transfer)
meta = files.meta("reports/q1.pdf")
# List files by prefix
entries = files.list(prefix="reports/", limit=50)
# entries = [{"name": "reports/q1.pdf", "size": 12345, "content_type": "..."}]
# Check existence
if files.exists("reports/q1.pdf"):
files.delete("reports/q1.pdf")
# Bulk delete
files.delete_prefix("temp/")
```
**Notes:**
- `content` accepts string or bytes; `get()` returns bytes.
- Maximum file size: 50 MB (configurable via `EXT_FILES_MAX_SIZE`).
- Metadata is stored as a companion JSON object, not in the file itself.
- `files.list()` automatically filters out internal metadata companions.
---
### workspace
**Permission:** `workspace.manage`
Managed disk directories for extensions that need a real filesystem
(git clones, compilers, media tools). Each workspace is scoped to
`{WORKSPACE_ROOT}/{packageID}/{name}/`.
```python
# Create a workspace (idempotent)
path = workspace.create("my-repo")
# Returns the absolute path to the directory
# Get the path (None if workspace doesn't exist)
path = workspace.path("my-repo")
# List all workspaces owned by this extension
names = workspace.list() # ["my-repo", "cache"]
# Delete a workspace and all its contents
workspace.delete("my-repo")
# Get disk usage in bytes (10-second timeout)
size = workspace.usage("my-repo") # 1048576
```
**Constraints:**
- Names must match `^[a-z][a-z0-9_]{0,62}$` (lowercase, no spaces or separators).
- Path traversal and symlink escape are blocked.
- Quota enforcement via `WORKSPACE_QUOTA_MB` env var (0 = unlimited).
- Module not available if `WORKSPACE_ROOT` is unset or not writable.
Use `settings.has_capability("workspace")` to check availability.
---
## Example: automated stage hook
A simple hook that reads a setting, queries data, and advances:
```python
def on_run(ctx):
threshold = settings.get("approval_threshold", 1000)
amount = ctx["stage_data"].get("amount", 0)
if amount > threshold:
notifications.send(
ctx["started_by"],
"High-value submission",
body="Amount %d exceeds threshold." % amount,
)
return {"advance": True, "data": {"needs_review": True}}
return {"advance": True, "data": {"needs_review": False}}
```