spell-runtime
v1.2.5
Published
Minimal CLI runtime for SpellBundle v1
Downloads
34
Readme
Spell Runtime v1
Minimal CLI runtime for SpellBundle v1.
Setup
- Node.js >= 20
- pnpm (recommended)
- npm (supported)
pnpm install
pnpm run build
pnpm testLocal dev:
pnpm run dev -- --helpInstall as CLI
Global install:
npm i -g spell-runtime
spell --helpRun with npx:
npx --yes --package spell-runtime spell --helpLocal package smoke checks:
npm run smoke:link
npm run smoke:npxCommands
spell install <source>spell listspell inspect <id> [--version x.y.z]spell cast <id> [--version x.y.z] [-p key=value ...] [--input input.json] [--dry-run] [--yes] [--allow-billing] [--allow-unsigned] [--require-signature] [--verbose] [--profile <name>]spell license add <name> <entitlement-token>spell license listspell license remove <name>spell sign keygen <publisher> [--key-id default] [--out-dir .spell-keys]spell sign bundle <local-path> --private-key <file> [--key-id default] [--publisher <name>]spell trust add <publisher> <public-key> [--key-id default]spell trust listspell trust remove <publisher>spell log <execution-id>
Install Sources
spell install <source> accepts:
- local bundle paths (existing behavior)
- pinned git URLs with an explicit ref suffix:
https://...#<ref>ssh://...#<ref>git@...#<ref>
Git sources must include #<ref>. If omitted, install fails with:
git source requires explicit ref (#<ref>)
When a git source is provided, runtime clones the repository, checks out the requested ref, resolves the checked-out commit SHA (git rev-parse HEAD), and installs from that checkout.
Limitations:
gitmust be installed and available onPATH.- clone/auth/network behavior is delegated to your local
gitconfiguration. spell.yamlmust exist at the cloned repository root (subdirectory installs are not supported).
Storage Layout
- Spells:
~/.spell/spells/<id_key>/<version>/ - ID index:
~/.spell/spells/<id_key>/spell.id.txt - Install provenance:
~/.spell/spells/<id_key>/<version>/source.json - Logs:
~/.spell/logs/<timestamp>_<id>_<version>.json - Billing entitlement records:
~/.spell/licenses/*.json
source.json captures install provenance:
type:localorgitsource: original install inputref: requested git ref (git installs only)commit: resolved git commit SHA (git installs only)installed_at: install timestamp (ISO-8601)
id_key is fixed as base64url(utf8(id)).
idis the logical identifier (display, package identity).id_keyis only for safe filesystem storage.
Consistency rule:
installchecksspell.yamlid againstspell.id.txtwhenspell.id.txtalready exists.- mismatch is treated as an error.
Cast Preflight
cast performs these checks before execution:
- bundle resolution by id (and optional version)
- input assembly (
--input+-poverrides) - JSON Schema validation by Ajv
- signature verification (default on; bypass only with
--allow-unsigned) - runtime policy guard (
~/.spell/policy.json) - platform guard
- risk guard (
high/criticalrequires--yes) - billing guard (
billing.enabledrequires--allow-billing) - billing entitlement guard (
billing.enabled+--allow-billingrequires a matching valid entitlement fromspell license add ...) - connector token guard (
CONNECTOR_<NAME>_TOKEN) - execution summary output
If --dry-run is set, command exits after summary and validation.
Policy file format (~/.spell/policy.json):
{
"version": "v1",
"default": "allow",
"publishers": { "allow": ["samples"], "deny": ["blocked"] },
"max_risk": "high",
"runtime": { "allow_execution": ["host", "docker"] }
}Notes:
- missing policy file => allow by default
- invalid policy file =>
invalid policy: ... - policy rejection =>
policy denied: <reason>
Runtime Safety Limits (v2 isolation)
cast enforces these runtime limits (used by direct CLI casts and API-triggered casts because the API invokes spell cast):
SPELL_RUNTIME_INPUT_MAX_BYTES(default65536): max bytes for merged cast input (--input+-poverrides).SPELL_RUNTIME_STEP_TIMEOUT_MS(default60000): max runtime pershellstep. On timeout, the runtime kills the step process and fails with the step name + timeout ms.SPELL_RUNTIME_EXECUTION_TIMEOUT_MS(default disabled): max total cast runtime across host/docker paths when set to an integer> 0.
Runtime Model
v1 supports:
- host: steps run in order, shell/http supported.
- docker: steps run in a linux container via "runner-in-image".
Docker mode (v1) details:
runtime.execution=dockerrequiresruntime.docker_image.- the image must provide
spell-runneronPATH(this repo publishes it as a second npm bin). - the bundle is mounted read-only at
/spell; the runner copies it into a writable temp workdir before executing steps. - hardened
docker rundefaults:--network none--cap-drop ALL--security-opt no-new-privileges--read-only--user 65532:65532--pids-limit 256--tmpfs /tmp:rw,noexec,nosuid,size=64m
- hardening env overrides (all optional):
SPELL_DOCKER_NETWORK(none|bridge|host, defaultnone)SPELL_DOCKER_USER(default65532:65532; set empty to disable--user)SPELL_DOCKER_READ_ONLY(1default; set0to disable--read-only)SPELL_DOCKER_PIDS_LIMIT(256default; set0to disable--pids-limit)SPELL_DOCKER_MEMORY(default empty; when set adds--memory)SPELL_DOCKER_CPUS(default empty; when set adds--cpus)
- environment variables passed from host -> container are restricted to connector tokens (
CONNECTOR_<NAME>_TOKEN) plusSPELL_RUNTIME_STEP_TIMEOUT_MS.
Windows Policy
- host mode does not assume bash/sh.
- shell step expects executable files (
.js/.exe/.cmd/.ps1etc). - process spawn uses
shell=false. - for strict cross-OS reproducibility, docker mode is the long-term recommended path.
Effects Vocabulary (Recommended)
Use these effect.type words where possible:
createupdatedeletedeploynotify
v1 Limitations (Intentionally Not Implemented)
- name search or ambiguous resolution (id only)
- registry/marketplace integration
- real billing execution (Stripe)
- DAG/parallel/rollback/self-healing
- advanced templating language (only
{{INPUT.*}}and{{ENV.*}}) - docker env passthrough beyond connector tokens and
SPELL_RUNTIME_STEP_TIMEOUT_MS
Signature (Sign + Verify)
spell cast requires signature verification by default. To bypass this for unsigned bundle workflows, use:
spell cast <id> --allow-unsigned ...--require-signature remains accepted for backward compatibility.
Signing flow:
spell sign keygen samples --key-id default --out-dir .spell-keys
spell trust add samples <public_key_base64url> --key-id default
spell sign bundle ./examples/spells/call-webhook --private-key .spell-keys/samples__default.private.pem --key-id defaultTrust store:
spell trust add <publisher> <public-key>spell trust listspell trust remove <publisher>
Notes:
- publisher is derived from the spell id prefix before the first
/(example:samples/call-webhook->samples). - public key format is ed25519
spkiDER encoded as base64url.
Entitlement Tokens (Billing)
spell license add <name> <token> now validates and stores signed entitlement tokens.
Token format:
ent1.<payloadBase64url>.<signatureBase64url>
Payload JSON required fields:
version("v1")issuer(string)key_id(string)mode("upfront" | "on_success" | "subscription")currency(string)max_amount(number)not_before(ISO string)expires_at(ISO string)
Verification rules:
- signature algorithm is ed25519
- signed message is the raw payload segment bytes (the exact payload base64url segment string bytes)
- trust source is the publisher trust store (
spell trust add ...) keyed byissuer + key_id - token must be within
not_before <= now <= expires_at
Billing-enabled cast requires a matching currently-valid entitlement:
- entitlement
mode === manifest.billing.mode - entitlement
currencyequalsmanifest.billing.currency(case-insensitive) - entitlement
max_amount >= manifest.billing.max_amount
spell license list prints entitlement summary columns (issuer, mode, currency, max_amount, expires_at) and never prints raw tokens.
Example Flow
spell install ./fixtures/spells/hello-host
spell list
spell inspect fixtures/hello-host
spell cast fixtures/hello-host --dry-run -p name=world
spell cast fixtures/hello-host -p name=world
spell log <execution-id>OSS Release (pnpm + GitHub Actions)
Release automation is defined in:
.github/workflows/release.yml
Prerequisites:
- npm account with
2FAenabled - GitHub repository secret
NPM_TOKEN(publish-capable npm token)
Local release checks:
pnpm install
pnpm run typecheck
pnpm run lint
pnpm run build
pnpm test
pnpm run pack:checkTag-based release flow:
npm version patch
git push --follow-tagsPushing tag vX.Y.Z triggers GitHub Actions release and runs npm publish.
The workflow verifies that the git tag version matches package.json.
Real-Use Sample Spells
These are product-facing examples (separate from test fixtures):
/Users/koichinishizuka/spell-runtime/examples/spells/call-webhook/Users/koichinishizuka/spell-runtime/examples/spells/repo-ops/Users/koichinishizuka/spell-runtime/examples/spells/publish-site
Quick try:
spell install ./examples/spells/call-webhook
spell inspect samples/call-webhook
spell cast samples/call-webhook --dry-run -p event=deploy -p source=manual -p payload='{"service":"web"}'UI Connection Spec
- Decision-complete button integration spec:
/Users/koichinishizuka/spell-runtime/docs/ui-connection-spec-v1.md
- Sample button registry:
/Users/koichinishizuka/spell-runtime/examples/button-registry.v1.json
- Button registry schema:
/Users/koichinishizuka/spell-runtime/examples/button-registry.v1.schema.json
- Registry optional policy:
require_signature:true: Execution API enforces signature (--require-signature)false/omitted: Execution API opts into unsigned path (--allow-unsigned)
Runtime Decision Log
/Users/koichinishizuka/spell-runtime/docs/runtime-decisions-v1.md
Repository Policies
/Users/koichinishizuka/spell-runtime/CONTRIBUTING.md/Users/koichinishizuka/spell-runtime/CODE_OF_CONDUCT.md/Users/koichinishizuka/spell-runtime/SECURITY.md
Execution API (Async)
Start API server:
npm run api:devBy default it listens on :8787 and reads:
- button registry:
./examples/button-registry.v1.json - limits:
- request body:
64KB - execution timeout:
60s - in-flight executions:
4
- request body:
- execution index persistence:
~/.spell/logs/index.json - routes:
GET /(minimal Receipts UI)GET /ui/app.js(UI client script)GET /api/buttonsGET /api/spell-executions(status,button_id,tenant_id,limitquery supported)POST /api/spell-executionsGET /api/spell-executions/:execution_id
Optional environment variables:
SPELL_API_PORTSPELL_BUTTON_REGISTRY_PATHSPELL_API_AUTH_KEYS(comma-separatedrole=tokenortenant:role=tokenentries; when set,/api/*requires auth and derivesactor_role+tenant_idfrom token)SPELL_API_AUTH_TOKENS(legacy: comma-separated tokens; when set,/api/*requires auth but does not bind role)SPELL_API_BODY_LIMIT_BYTESSPELL_API_EXECUTION_TIMEOUT_MSSPELL_API_RATE_LIMIT_WINDOW_MSSPELL_API_RATE_LIMIT_MAX_REQUESTSSPELL_API_MAX_CONCURRENT_EXECUTIONSSPELL_API_LOG_RETENTION_DAYS(default14,0disables age-based pruning)SPELL_API_LOG_MAX_FILES(default500,0disables count-based pruning)
Security note:
- execution logs redact secret-like keys (
token,authorization,apiKey, etc.) - environment-derived secret values are masked in persisted logs
- when auth is enabled, pass
Authorization: Bearer <token>(orx-api-key) for/apiroutes - with
SPELL_API_AUTH_KEYS, non-admin list requests are restricted to their own tenant and cross-tenanttenant_idfilters return403(TENANT_FORBIDDEN) - do not set both
SPELL_API_AUTH_KEYSandSPELL_API_AUTH_TOKENSat the same time
