spell-runtime 1.2.0 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -16
- package/README.txt +145 -16
- package/dist/api/server.js +128 -21
- package/dist/api/server.js.map +1 -1
- package/dist/bundle/install.d.ts +1 -1
- package/dist/bundle/install.js +120 -6
- package/dist/bundle/install.js.map +1 -1
- package/dist/cli/index.js +44 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/license/entitlement.d.ts +28 -0
- package/dist/license/entitlement.js +174 -0
- package/dist/license/entitlement.js.map +1 -0
- package/dist/license/store.d.ts +16 -0
- package/dist/license/store.js +271 -0
- package/dist/license/store.js.map +1 -0
- package/dist/policy/index.d.ts +26 -0
- package/dist/policy/index.js +194 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/runner/cast.js +37 -3
- package/dist/runner/cast.js.map +1 -1
- package/dist/runner/dockerRunner.d.ts +2 -1
- package/dist/runner/dockerRunner.js +148 -13
- package/dist/runner/dockerRunner.js.map +1 -1
- package/dist/runner/hostRunner.d.ts +1 -1
- package/dist/runner/hostRunner.js +30 -3
- package/dist/runner/hostRunner.js.map +1 -1
- package/dist/runner/runtimeLimits.d.ts +6 -0
- package/dist/runner/runtimeLimits.js +48 -0
- package/dist/runner/runtimeLimits.js.map +1 -0
- package/dist/runner/spell-runner.js +0 -0
- package/dist/steps/httpStep.d.ts +1 -1
- package/dist/steps/httpStep.js +20 -6
- package/dist/steps/httpStep.js.map +1 -1
- package/dist/steps/shellStep.d.ts +5 -1
- package/dist/steps/shellStep.js +33 -7
- package/dist/steps/shellStep.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/util/paths.d.ts +1 -0
- package/dist/util/paths.js +5 -0
- package/dist/util/paths.js.map +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -5,18 +5,19 @@ Minimal CLI runtime for SpellBundle v1.
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
7
|
- Node.js >= 20
|
|
8
|
-
-
|
|
8
|
+
- pnpm (recommended)
|
|
9
|
+
- npm (supported)
|
|
9
10
|
|
|
10
11
|
```bash
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
pnpm install
|
|
13
|
+
pnpm run build
|
|
14
|
+
pnpm test
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
Local dev:
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
|
-
|
|
20
|
+
pnpm run dev -- --help
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
## Install as CLI
|
|
@@ -43,10 +44,13 @@ npm run smoke:npx
|
|
|
43
44
|
|
|
44
45
|
## Commands
|
|
45
46
|
|
|
46
|
-
- `spell install <
|
|
47
|
+
- `spell install <source>`
|
|
47
48
|
- `spell list`
|
|
48
49
|
- `spell inspect <id> [--version x.y.z]`
|
|
49
|
-
- `spell cast <id> [--version x.y.z] [-p key=value ...] [--input input.json] [--dry-run] [--yes] [--allow-billing] [--require-signature] [--verbose] [--profile <name>]`
|
|
50
|
+
- `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>]`
|
|
51
|
+
- `spell license add <name> <entitlement-token>`
|
|
52
|
+
- `spell license list`
|
|
53
|
+
- `spell license remove <name>`
|
|
50
54
|
- `spell sign keygen <publisher> [--key-id default] [--out-dir .spell-keys]`
|
|
51
55
|
- `spell sign bundle <local-path> --private-key <file> [--key-id default] [--publisher <name>]`
|
|
52
56
|
- `spell trust add <publisher> <public-key> [--key-id default]`
|
|
@@ -54,11 +58,43 @@ npm run smoke:npx
|
|
|
54
58
|
- `spell trust remove <publisher>`
|
|
55
59
|
- `spell log <execution-id>`
|
|
56
60
|
|
|
61
|
+
## Install Sources
|
|
62
|
+
|
|
63
|
+
`spell install <source>` accepts:
|
|
64
|
+
|
|
65
|
+
- local bundle paths (existing behavior)
|
|
66
|
+
- pinned git URLs with an explicit ref suffix:
|
|
67
|
+
- `https://...#<ref>`
|
|
68
|
+
- `ssh://...#<ref>`
|
|
69
|
+
- `git@...#<ref>`
|
|
70
|
+
|
|
71
|
+
Git sources must include `#<ref>`. If omitted, install fails with:
|
|
72
|
+
|
|
73
|
+
- `git source requires explicit ref (#<ref>)`
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
Limitations:
|
|
78
|
+
|
|
79
|
+
- `git` must be installed and available on `PATH`.
|
|
80
|
+
- clone/auth/network behavior is delegated to your local `git` configuration.
|
|
81
|
+
- `spell.yaml` must exist at the cloned repository root (subdirectory installs are not supported).
|
|
82
|
+
|
|
57
83
|
## Storage Layout
|
|
58
84
|
|
|
59
85
|
- Spells: `~/.spell/spells/<id_key>/<version>/`
|
|
60
86
|
- ID index: `~/.spell/spells/<id_key>/spell.id.txt`
|
|
87
|
+
- Install provenance: `~/.spell/spells/<id_key>/<version>/source.json`
|
|
61
88
|
- Logs: `~/.spell/logs/<timestamp>_<id>_<version>.json`
|
|
89
|
+
- Billing entitlement records: `~/.spell/licenses/*.json`
|
|
90
|
+
|
|
91
|
+
`source.json` captures install provenance:
|
|
92
|
+
|
|
93
|
+
- `type`: `local` or `git`
|
|
94
|
+
- `source`: original install input
|
|
95
|
+
- `ref`: requested git ref (git installs only)
|
|
96
|
+
- `commit`: resolved git commit SHA (git installs only)
|
|
97
|
+
- `installed_at`: install timestamp (ISO-8601)
|
|
62
98
|
|
|
63
99
|
`id_key` is fixed as `base64url(utf8(id))`.
|
|
64
100
|
|
|
@@ -77,15 +113,42 @@ Consistency rule:
|
|
|
77
113
|
- bundle resolution by id (and optional version)
|
|
78
114
|
- input assembly (`--input` + `-p` overrides)
|
|
79
115
|
- JSON Schema validation by Ajv
|
|
80
|
-
-
|
|
116
|
+
- signature verification (default on; bypass only with `--allow-unsigned`)
|
|
117
|
+
- runtime policy guard (`~/.spell/policy.json`)
|
|
81
118
|
- platform guard
|
|
82
119
|
- risk guard (`high`/`critical` requires `--yes`)
|
|
83
120
|
- billing guard (`billing.enabled` requires `--allow-billing`)
|
|
121
|
+
- billing entitlement guard (`billing.enabled` + `--allow-billing` requires a matching valid entitlement from `spell license add ...`)
|
|
84
122
|
- connector token guard (`CONNECTOR_<NAME>_TOKEN`)
|
|
85
123
|
- execution summary output
|
|
86
124
|
|
|
87
125
|
If `--dry-run` is set, command exits after summary and validation.
|
|
88
126
|
|
|
127
|
+
Policy file format (`~/.spell/policy.json`):
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"version": "v1",
|
|
132
|
+
"default": "allow",
|
|
133
|
+
"publishers": { "allow": ["samples"], "deny": ["blocked"] },
|
|
134
|
+
"max_risk": "high",
|
|
135
|
+
"runtime": { "allow_execution": ["host", "docker"] }
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Notes:
|
|
140
|
+
- missing policy file => allow by default
|
|
141
|
+
- invalid policy file => `invalid policy: ...`
|
|
142
|
+
- policy rejection => `policy denied: <reason>`
|
|
143
|
+
|
|
144
|
+
## Runtime Safety Limits (v2 isolation)
|
|
145
|
+
|
|
146
|
+
`cast` enforces these runtime limits (used by direct CLI casts and API-triggered casts because the API invokes `spell cast`):
|
|
147
|
+
|
|
148
|
+
- `SPELL_RUNTIME_INPUT_MAX_BYTES` (default `65536`): max bytes for merged cast input (`--input` + `-p` overrides).
|
|
149
|
+
- `SPELL_RUNTIME_STEP_TIMEOUT_MS` (default `60000`): max runtime per `shell` step. On timeout, the runtime kills the step process and fails with the step name + timeout ms.
|
|
150
|
+
- `SPELL_RUNTIME_EXECUTION_TIMEOUT_MS` (default disabled): max total cast runtime across host/docker paths when set to an integer `> 0`.
|
|
151
|
+
|
|
89
152
|
## Runtime Model
|
|
90
153
|
|
|
91
154
|
v1 supports:
|
|
@@ -98,7 +161,22 @@ Docker mode (v1) details:
|
|
|
98
161
|
- `runtime.execution=docker` requires `runtime.docker_image`.
|
|
99
162
|
- the image must provide `spell-runner` on `PATH` (this repo publishes it as a second npm bin).
|
|
100
163
|
- the bundle is mounted read-only at `/spell`; the runner copies it into a writable temp workdir before executing steps.
|
|
101
|
-
-
|
|
164
|
+
- hardened `docker run` defaults:
|
|
165
|
+
- `--network none`
|
|
166
|
+
- `--cap-drop ALL`
|
|
167
|
+
- `--security-opt no-new-privileges`
|
|
168
|
+
- `--read-only`
|
|
169
|
+
- `--user 65532:65532`
|
|
170
|
+
- `--pids-limit 256`
|
|
171
|
+
- `--tmpfs /tmp:rw,noexec,nosuid,size=64m`
|
|
172
|
+
- hardening env overrides (all optional):
|
|
173
|
+
- `SPELL_DOCKER_NETWORK` (`none|bridge|host`, default `none`)
|
|
174
|
+
- `SPELL_DOCKER_USER` (default `65532:65532`; set empty to disable `--user`)
|
|
175
|
+
- `SPELL_DOCKER_READ_ONLY` (`1` default; set `0` to disable `--read-only`)
|
|
176
|
+
- `SPELL_DOCKER_PIDS_LIMIT` (`256` default; set `0` to disable `--pids-limit`)
|
|
177
|
+
- `SPELL_DOCKER_MEMORY` (default empty; when set adds `--memory`)
|
|
178
|
+
- `SPELL_DOCKER_CPUS` (default empty; when set adds `--cpus`)
|
|
179
|
+
- environment variables passed from host -> container are restricted to connector tokens (`CONNECTOR_<NAME>_TOKEN`) plus `SPELL_RUNTIME_STEP_TIMEOUT_MS`.
|
|
102
180
|
|
|
103
181
|
## Windows Policy
|
|
104
182
|
|
|
@@ -120,20 +198,22 @@ Use these `effect.type` words where possible:
|
|
|
120
198
|
## v1 Limitations (Intentionally Not Implemented)
|
|
121
199
|
|
|
122
200
|
- name search or ambiguous resolution (id only)
|
|
123
|
-
- registry/marketplace
|
|
201
|
+
- registry/marketplace integration
|
|
124
202
|
- real billing execution (Stripe)
|
|
125
203
|
- DAG/parallel/rollback/self-healing
|
|
126
204
|
- advanced templating language (only `{{INPUT.*}}` and `{{ENV.*}}`)
|
|
127
|
-
- docker env passthrough beyond connector tokens
|
|
205
|
+
- docker env passthrough beyond connector tokens and `SPELL_RUNTIME_STEP_TIMEOUT_MS`
|
|
128
206
|
|
|
129
207
|
## Signature (Sign + Verify)
|
|
130
208
|
|
|
131
|
-
|
|
209
|
+
`spell cast` requires signature verification by default. To bypass this for unsigned bundle workflows, use:
|
|
132
210
|
|
|
133
211
|
```bash
|
|
134
|
-
spell cast <id> --
|
|
212
|
+
spell cast <id> --allow-unsigned ...
|
|
135
213
|
```
|
|
136
214
|
|
|
215
|
+
`--require-signature` remains accepted for backward compatibility.
|
|
216
|
+
|
|
137
217
|
Signing flow:
|
|
138
218
|
|
|
139
219
|
```bash
|
|
@@ -153,6 +233,40 @@ Notes:
|
|
|
153
233
|
- publisher is derived from the spell id prefix before the first `/` (example: `samples/call-webhook` -> `samples`).
|
|
154
234
|
- public key format is ed25519 `spki` DER encoded as base64url.
|
|
155
235
|
|
|
236
|
+
## Entitlement Tokens (Billing)
|
|
237
|
+
|
|
238
|
+
`spell license add <name> <token>` now validates and stores signed entitlement tokens.
|
|
239
|
+
|
|
240
|
+
Token format:
|
|
241
|
+
|
|
242
|
+
- `ent1.<payloadBase64url>.<signatureBase64url>`
|
|
243
|
+
|
|
244
|
+
Payload JSON required fields:
|
|
245
|
+
|
|
246
|
+
- `version` (`"v1"`)
|
|
247
|
+
- `issuer` (string)
|
|
248
|
+
- `key_id` (string)
|
|
249
|
+
- `mode` (`"upfront" | "on_success" | "subscription"`)
|
|
250
|
+
- `currency` (string)
|
|
251
|
+
- `max_amount` (number)
|
|
252
|
+
- `not_before` (ISO string)
|
|
253
|
+
- `expires_at` (ISO string)
|
|
254
|
+
|
|
255
|
+
Verification rules:
|
|
256
|
+
|
|
257
|
+
- signature algorithm is ed25519
|
|
258
|
+
- signed message is the raw payload segment bytes (the exact payload base64url segment string bytes)
|
|
259
|
+
- trust source is the publisher trust store (`spell trust add ...`) keyed by `issuer + key_id`
|
|
260
|
+
- token must be within `not_before <= now <= expires_at`
|
|
261
|
+
|
|
262
|
+
Billing-enabled cast requires a matching currently-valid entitlement:
|
|
263
|
+
|
|
264
|
+
- entitlement `mode === manifest.billing.mode`
|
|
265
|
+
- entitlement `currency` equals `manifest.billing.currency` (case-insensitive)
|
|
266
|
+
- entitlement `max_amount >= manifest.billing.max_amount`
|
|
267
|
+
|
|
268
|
+
`spell license list` prints entitlement summary columns (`issuer`, `mode`, `currency`, `max_amount`, `expires_at`) and never prints raw tokens.
|
|
269
|
+
|
|
156
270
|
## Example Flow
|
|
157
271
|
|
|
158
272
|
```bash
|
|
@@ -164,6 +278,38 @@ spell cast fixtures/hello-host -p name=world
|
|
|
164
278
|
spell log <execution-id>
|
|
165
279
|
```
|
|
166
280
|
|
|
281
|
+
## OSS Release (pnpm + GitHub Actions)
|
|
282
|
+
|
|
283
|
+
Release automation is defined in:
|
|
284
|
+
|
|
285
|
+
- `.github/workflows/release.yml`
|
|
286
|
+
|
|
287
|
+
Prerequisites:
|
|
288
|
+
|
|
289
|
+
- npm account with `2FA` enabled
|
|
290
|
+
- GitHub repository secret `NPM_TOKEN` (publish-capable npm token)
|
|
291
|
+
|
|
292
|
+
Local release checks:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
pnpm install
|
|
296
|
+
pnpm run typecheck
|
|
297
|
+
pnpm run lint
|
|
298
|
+
pnpm run build
|
|
299
|
+
pnpm test
|
|
300
|
+
pnpm run pack:check
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Tag-based release flow:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm version patch
|
|
307
|
+
git push --follow-tags
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Pushing tag `vX.Y.Z` triggers GitHub Actions release and runs `npm publish`.
|
|
311
|
+
The workflow verifies that the git tag version matches `package.json`.
|
|
312
|
+
|
|
167
313
|
## Real-Use Sample Spells
|
|
168
314
|
|
|
169
315
|
These are product-facing examples (separate from test fixtures):
|
|
@@ -189,12 +335,20 @@ spell cast samples/call-webhook --dry-run -p event=deploy -p source=manual -p pa
|
|
|
189
335
|
- Button registry schema:
|
|
190
336
|
- `/Users/koichinishizuka/spell-runtime/examples/button-registry.v1.schema.json`
|
|
191
337
|
- Registry optional policy:
|
|
192
|
-
- `require_signature
|
|
338
|
+
- `require_signature`:
|
|
339
|
+
- `true`: Execution API enforces signature (`--require-signature`)
|
|
340
|
+
- `false`/omitted: Execution API opts into unsigned path (`--allow-unsigned`)
|
|
193
341
|
|
|
194
342
|
## Runtime Decision Log
|
|
195
343
|
|
|
196
344
|
- `/Users/koichinishizuka/spell-runtime/docs/runtime-decisions-v1.md`
|
|
197
345
|
|
|
346
|
+
## Repository Policies
|
|
347
|
+
|
|
348
|
+
- `/Users/koichinishizuka/spell-runtime/CONTRIBUTING.md`
|
|
349
|
+
- `/Users/koichinishizuka/spell-runtime/CODE_OF_CONDUCT.md`
|
|
350
|
+
- `/Users/koichinishizuka/spell-runtime/SECURITY.md`
|
|
351
|
+
|
|
198
352
|
## Execution API (Async)
|
|
199
353
|
|
|
200
354
|
Start API server:
|
|
@@ -214,14 +368,14 @@ By default it listens on `:8787` and reads:
|
|
|
214
368
|
- `GET /` (minimal Receipts UI)
|
|
215
369
|
- `GET /ui/app.js` (UI client script)
|
|
216
370
|
- `GET /api/buttons`
|
|
217
|
-
- `GET /api/spell-executions` (`status`, `button_id`, `limit` query supported)
|
|
371
|
+
- `GET /api/spell-executions` (`status`, `button_id`, `tenant_id`, `limit` query supported)
|
|
218
372
|
- `POST /api/spell-executions`
|
|
219
373
|
- `GET /api/spell-executions/:execution_id`
|
|
220
374
|
|
|
221
375
|
Optional environment variables:
|
|
222
376
|
- `SPELL_API_PORT`
|
|
223
377
|
- `SPELL_BUTTON_REGISTRY_PATH`
|
|
224
|
-
- `SPELL_API_AUTH_KEYS` (comma-separated `role=token` entries; when set, `/api/*` requires auth and derives `actor_role` from token)
|
|
378
|
+
- `SPELL_API_AUTH_KEYS` (comma-separated `role=token` or `tenant:role=token` entries; when set, `/api/*` requires auth and derives `actor_role` + `tenant_id` from token)
|
|
225
379
|
- `SPELL_API_AUTH_TOKENS` (legacy: comma-separated tokens; when set, `/api/*` requires auth but does not bind role)
|
|
226
380
|
- `SPELL_API_BODY_LIMIT_BYTES`
|
|
227
381
|
- `SPELL_API_EXECUTION_TIMEOUT_MS`
|
|
@@ -235,4 +389,5 @@ Security note:
|
|
|
235
389
|
- execution logs redact secret-like keys (`token`, `authorization`, `apiKey`, etc.)
|
|
236
390
|
- environment-derived secret values are masked in persisted logs
|
|
237
391
|
- when auth is enabled, pass `Authorization: Bearer <token>` (or `x-api-key`) for `/api` routes
|
|
392
|
+
- with `SPELL_API_AUTH_KEYS`, non-admin list requests are restricted to their own tenant and cross-tenant `tenant_id` filters return `403` (`TENANT_FORBIDDEN`)
|
|
238
393
|
- do not set both `SPELL_API_AUTH_KEYS` and `SPELL_API_AUTH_TOKENS` at the same time
|
package/README.txt
CHANGED
|
@@ -4,19 +4,20 @@ Minimal CLI runtime for SpellBundle v1.
|
|
|
4
4
|
|
|
5
5
|
1. Setup
|
|
6
6
|
- Node.js >= 20
|
|
7
|
-
-
|
|
7
|
+
- pnpm (recommended)
|
|
8
|
+
- npm (supported)
|
|
8
9
|
|
|
9
10
|
Install dependencies:
|
|
10
|
-
|
|
11
|
+
pnpm install
|
|
11
12
|
|
|
12
13
|
Build:
|
|
13
|
-
|
|
14
|
+
pnpm run build
|
|
14
15
|
|
|
15
16
|
Test:
|
|
16
|
-
|
|
17
|
+
pnpm test
|
|
17
18
|
|
|
18
19
|
Local dev:
|
|
19
|
-
|
|
20
|
+
pnpm run dev -- --help
|
|
20
21
|
|
|
21
22
|
Binary smoke checks:
|
|
22
23
|
npm run smoke:link
|
|
@@ -30,10 +31,13 @@ Manual npx (local package):
|
|
|
30
31
|
npx --yes --package file:. spell --help
|
|
31
32
|
|
|
32
33
|
2. CLI commands
|
|
33
|
-
- spell install <
|
|
34
|
+
- spell install <source>
|
|
34
35
|
- spell list
|
|
35
36
|
- spell inspect <id> [--version x.y.z]
|
|
36
|
-
- spell cast <id> [--version x.y.z] [-p key=value ...] [--input input.json] [--dry-run] [--yes] [--allow-billing] [--require-signature] [--verbose] [--profile <name>]
|
|
37
|
+
- 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>]
|
|
38
|
+
- spell license add <name> <entitlement-token>
|
|
39
|
+
- spell license list
|
|
40
|
+
- spell license remove <name>
|
|
37
41
|
- spell sign keygen <publisher> [--key-id default] [--out-dir .spell-keys]
|
|
38
42
|
- spell sign bundle <local-path> --private-key <file> [--key-id default] [--publisher <name>]
|
|
39
43
|
- spell trust add <publisher> <public-key> [--key-id default]
|
|
@@ -41,10 +45,37 @@ Manual npx (local package):
|
|
|
41
45
|
- spell trust remove <publisher>
|
|
42
46
|
- spell log <execution-id>
|
|
43
47
|
|
|
48
|
+
2.1 Install sources
|
|
49
|
+
- spell install <source> accepts:
|
|
50
|
+
- local bundle paths (existing behavior)
|
|
51
|
+
- pinned git URLs with explicit refs:
|
|
52
|
+
- https://...#<ref>
|
|
53
|
+
- ssh://...#<ref>
|
|
54
|
+
- git@...#<ref>
|
|
55
|
+
|
|
56
|
+
Git sources must include #<ref>. If omitted, install fails with:
|
|
57
|
+
git source requires explicit ref (#<ref>)
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
Limitations:
|
|
62
|
+
- git must be installed and available on PATH.
|
|
63
|
+
- clone/auth/network behavior is delegated to your local git configuration.
|
|
64
|
+
- spell.yaml must exist at the cloned repository root (subdirectory installs are not supported).
|
|
65
|
+
|
|
44
66
|
3. Storage layout
|
|
45
67
|
- Spells: ~/.spell/spells/<id_key>/<version>/
|
|
46
68
|
- ID index: ~/.spell/spells/<id_key>/spell.id.txt
|
|
69
|
+
- Install provenance: ~/.spell/spells/<id_key>/<version>/source.json
|
|
47
70
|
- Logs: ~/.spell/logs/<timestamp>_<id>_<version>.json
|
|
71
|
+
- Billing entitlement records: ~/.spell/licenses/*.json
|
|
72
|
+
|
|
73
|
+
source.json captures install provenance:
|
|
74
|
+
- type: local or git
|
|
75
|
+
- source: original install input
|
|
76
|
+
- ref: requested git ref (git installs only)
|
|
77
|
+
- commit: resolved git commit SHA (git installs only)
|
|
78
|
+
- installed_at: install timestamp (ISO-8601)
|
|
48
79
|
|
|
49
80
|
id_key is fixed as base64url(utf8(id)).
|
|
50
81
|
- id is the logical identifier (display, package identity).
|
|
@@ -59,15 +90,37 @@ Cast performs these checks before execution:
|
|
|
59
90
|
- Bundle resolution by id (and optional version)
|
|
60
91
|
- Input assembly (--input + -p overrides)
|
|
61
92
|
- JSON Schema validation by Ajv
|
|
62
|
-
-
|
|
93
|
+
- Signature verification (default on; bypass only with --allow-unsigned)
|
|
94
|
+
- Runtime policy guard (~/.spell/policy.json)
|
|
63
95
|
- Platform guard
|
|
64
96
|
- Risk guard (high/critical requires --yes)
|
|
65
97
|
- Billing guard (billing.enabled requires --allow-billing)
|
|
98
|
+
- Billing entitlement guard (billing.enabled + --allow-billing requires a matching valid entitlement from spell license add ...)
|
|
66
99
|
- Connector token guard (CONNECTOR_<NAME>_TOKEN)
|
|
67
100
|
- Execution summary output
|
|
68
101
|
|
|
69
102
|
If --dry-run is set, command exits after summary and validation.
|
|
70
103
|
|
|
104
|
+
Policy file format (~/.spell/policy.json):
|
|
105
|
+
{
|
|
106
|
+
"version": "v1",
|
|
107
|
+
"default": "allow",
|
|
108
|
+
"publishers": { "allow": ["samples"], "deny": ["blocked"] },
|
|
109
|
+
"max_risk": "high",
|
|
110
|
+
"runtime": { "allow_execution": ["host", "docker"] }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Notes:
|
|
114
|
+
- missing policy file => allow by default
|
|
115
|
+
- invalid policy file => invalid policy: ...
|
|
116
|
+
- policy rejection => policy denied: <reason>
|
|
117
|
+
|
|
118
|
+
4.1 Runtime safety limits (v2 isolation)
|
|
119
|
+
cast enforces these runtime limits (for direct CLI casts and API-triggered casts, because the API invokes spell cast):
|
|
120
|
+
- SPELL_RUNTIME_INPUT_MAX_BYTES (default 65536): max bytes for merged cast input (--input + -p overrides)
|
|
121
|
+
- SPELL_RUNTIME_STEP_TIMEOUT_MS (default 60000): max runtime per shell step; timed out step process is killed and cast fails with step name + timeout ms
|
|
122
|
+
- SPELL_RUNTIME_EXECUTION_TIMEOUT_MS (default disabled): max total cast runtime when set to an integer > 0 (enforced in host and docker paths)
|
|
123
|
+
|
|
71
124
|
5. Runtime model
|
|
72
125
|
v1 supports:
|
|
73
126
|
- host: steps run in order, shell/http supported.
|
|
@@ -77,7 +130,22 @@ Docker mode (v1) details:
|
|
|
77
130
|
- runtime.execution=docker requires runtime.docker_image.
|
|
78
131
|
- the image must provide spell-runner on PATH (this repo publishes it as a second npm bin).
|
|
79
132
|
- the bundle is mounted read-only at /spell; the runner copies it into a writable temp workdir before executing steps.
|
|
80
|
-
-
|
|
133
|
+
- hardened docker run defaults:
|
|
134
|
+
- --network none
|
|
135
|
+
- --cap-drop ALL
|
|
136
|
+
- --security-opt no-new-privileges
|
|
137
|
+
- --read-only
|
|
138
|
+
- --user 65532:65532
|
|
139
|
+
- --pids-limit 256
|
|
140
|
+
- --tmpfs /tmp:rw,noexec,nosuid,size=64m
|
|
141
|
+
- hardening env overrides (all optional):
|
|
142
|
+
- SPELL_DOCKER_NETWORK (none|bridge|host, default none)
|
|
143
|
+
- SPELL_DOCKER_USER (default 65532:65532; set empty to disable --user)
|
|
144
|
+
- SPELL_DOCKER_READ_ONLY (1 default; set 0 to disable --read-only)
|
|
145
|
+
- SPELL_DOCKER_PIDS_LIMIT (256 default; set 0 to disable --pids-limit)
|
|
146
|
+
- SPELL_DOCKER_MEMORY (default empty; when set adds --memory)
|
|
147
|
+
- SPELL_DOCKER_CPUS (default empty; when set adds --cpus)
|
|
148
|
+
- env vars passed from host -> container are restricted to connector tokens (CONNECTOR_<NAME>_TOKEN) plus SPELL_RUNTIME_STEP_TIMEOUT_MS.
|
|
81
149
|
|
|
82
150
|
6. Windows policy
|
|
83
151
|
- host mode does not assume bash/sh.
|
|
@@ -95,15 +163,17 @@ Use these effect.type words where possible:
|
|
|
95
163
|
|
|
96
164
|
8. v1 limitations (intentionally not implemented)
|
|
97
165
|
- name search or ambiguous resolution (id only)
|
|
98
|
-
- registry/marketplace
|
|
166
|
+
- registry/marketplace integration
|
|
99
167
|
- real billing execution (Stripe)
|
|
100
168
|
- DAG/parallel/rollback/self-healing
|
|
101
169
|
- advanced templating language (only {{INPUT.*}} and {{ENV.*}})
|
|
102
|
-
- docker env passthrough beyond connector tokens
|
|
170
|
+
- docker env passthrough beyond connector tokens and SPELL_RUNTIME_STEP_TIMEOUT_MS
|
|
103
171
|
|
|
104
172
|
8.1 Signature (sign + verify)
|
|
105
|
-
|
|
106
|
-
spell cast <id> --
|
|
173
|
+
spell cast requires signature verification by default. To bypass this for unsigned bundle workflows:
|
|
174
|
+
spell cast <id> --allow-unsigned ...
|
|
175
|
+
|
|
176
|
+
--require-signature remains accepted for backward compatibility.
|
|
107
177
|
|
|
108
178
|
Signing flow:
|
|
109
179
|
spell sign keygen samples --key-id default --out-dir .spell-keys
|
|
@@ -119,6 +189,35 @@ Notes:
|
|
|
119
189
|
- publisher is derived from the spell id prefix before the first / (example: samples/call-webhook -> samples).
|
|
120
190
|
- public key format is ed25519 spki DER encoded as base64url.
|
|
121
191
|
|
|
192
|
+
8.2 Entitlement tokens (billing)
|
|
193
|
+
spell license add <name> <token> now validates and stores signed entitlement tokens.
|
|
194
|
+
|
|
195
|
+
Token format:
|
|
196
|
+
- ent1.<payloadBase64url>.<signatureBase64url>
|
|
197
|
+
|
|
198
|
+
Payload JSON required fields:
|
|
199
|
+
- version ("v1")
|
|
200
|
+
- issuer (string)
|
|
201
|
+
- key_id (string)
|
|
202
|
+
- mode ("upfront" | "on_success" | "subscription")
|
|
203
|
+
- currency (string)
|
|
204
|
+
- max_amount (number)
|
|
205
|
+
- not_before (ISO string)
|
|
206
|
+
- expires_at (ISO string)
|
|
207
|
+
|
|
208
|
+
Verification rules:
|
|
209
|
+
- signature algorithm: ed25519
|
|
210
|
+
- signed message: raw payload segment bytes (exact payload base64url segment string bytes)
|
|
211
|
+
- trust source: publisher trust store (spell trust add ...) keyed by issuer + key_id
|
|
212
|
+
- token must be within not_before <= now <= expires_at
|
|
213
|
+
|
|
214
|
+
Billing-enabled cast requires a matching currently-valid entitlement:
|
|
215
|
+
- entitlement mode equals manifest.billing.mode
|
|
216
|
+
- entitlement currency equals manifest.billing.currency (case-insensitive)
|
|
217
|
+
- entitlement max_amount >= manifest.billing.max_amount
|
|
218
|
+
|
|
219
|
+
spell license list prints entitlement summary columns (issuer/mode/currency/max_amount/expires_at) and does not print raw tokens.
|
|
220
|
+
|
|
122
221
|
9. Example flow
|
|
123
222
|
1) Install a local fixture
|
|
124
223
|
spell install ./fixtures/spells/hello-host
|
|
@@ -138,6 +237,29 @@ Notes:
|
|
|
138
237
|
6) Show execution log
|
|
139
238
|
spell log <execution-id>
|
|
140
239
|
|
|
240
|
+
9.1 OSS release (pnpm + GitHub Actions)
|
|
241
|
+
Automation file:
|
|
242
|
+
.github/workflows/release.yml
|
|
243
|
+
|
|
244
|
+
Prerequisites:
|
|
245
|
+
- npm account with 2FA enabled
|
|
246
|
+
- GitHub repository secret NPM_TOKEN (publish-capable npm token)
|
|
247
|
+
|
|
248
|
+
Local release checks:
|
|
249
|
+
pnpm install
|
|
250
|
+
pnpm run typecheck
|
|
251
|
+
pnpm run lint
|
|
252
|
+
pnpm run build
|
|
253
|
+
pnpm test
|
|
254
|
+
pnpm run pack:check
|
|
255
|
+
|
|
256
|
+
Tag-based release flow:
|
|
257
|
+
npm version patch
|
|
258
|
+
git push --follow-tags
|
|
259
|
+
|
|
260
|
+
Tag push vX.Y.Z triggers GitHub Actions release and runs npm publish.
|
|
261
|
+
The workflow verifies that tag version matches package.json.
|
|
262
|
+
|
|
141
263
|
10. UI connection spec
|
|
142
264
|
- Decision-complete button integration spec:
|
|
143
265
|
/Users/koichinishizuka/spell-runtime/docs/ui-connection-spec-v1.md
|
|
@@ -146,7 +268,8 @@ Notes:
|
|
|
146
268
|
- Button registry schema:
|
|
147
269
|
/Users/koichinishizuka/spell-runtime/examples/button-registry.v1.schema.json
|
|
148
270
|
- Registry optional policy:
|
|
149
|
-
require_signature
|
|
271
|
+
require_signature=true: Execution API enforces signature (--require-signature)
|
|
272
|
+
require_signature=false or omitted: Execution API opts into unsigned path (--allow-unsigned)
|
|
150
273
|
|
|
151
274
|
11. Install from npm
|
|
152
275
|
Global install:
|
|
@@ -169,6 +292,11 @@ Quick try:
|
|
|
169
292
|
13. Runtime decision log
|
|
170
293
|
- /Users/koichinishizuka/spell-runtime/docs/runtime-decisions-v1.md
|
|
171
294
|
|
|
295
|
+
13.1 Repository policies
|
|
296
|
+
- /Users/koichinishizuka/spell-runtime/CONTRIBUTING.md
|
|
297
|
+
- /Users/koichinishizuka/spell-runtime/CODE_OF_CONDUCT.md
|
|
298
|
+
- /Users/koichinishizuka/spell-runtime/SECURITY.md
|
|
299
|
+
|
|
172
300
|
14. Execution API (async)
|
|
173
301
|
Start:
|
|
174
302
|
npm run api:dev
|
|
@@ -185,14 +313,14 @@ Defaults:
|
|
|
185
313
|
GET /
|
|
186
314
|
GET /ui/app.js
|
|
187
315
|
GET /api/buttons
|
|
188
|
-
GET /api/spell-executions (status/button_id/limit query supported)
|
|
316
|
+
GET /api/spell-executions (status/button_id/tenant_id/limit query supported)
|
|
189
317
|
POST /api/spell-executions
|
|
190
318
|
GET /api/spell-executions/:execution_id
|
|
191
319
|
|
|
192
320
|
Optional environment variables:
|
|
193
321
|
- SPELL_API_PORT
|
|
194
322
|
- SPELL_BUTTON_REGISTRY_PATH
|
|
195
|
-
- SPELL_API_AUTH_KEYS (comma-separated role=token entries; when set, /api/* requires auth and derives actor_role from token)
|
|
323
|
+
- SPELL_API_AUTH_KEYS (comma-separated role=token or tenant:role=token entries; when set, /api/* requires auth and derives actor_role + tenant_id from token)
|
|
196
324
|
- SPELL_API_AUTH_TOKENS (legacy: comma-separated tokens; when set, /api/* requires auth but does not bind role)
|
|
197
325
|
- SPELL_API_BODY_LIMIT_BYTES
|
|
198
326
|
- SPELL_API_EXECUTION_TIMEOUT_MS
|
|
@@ -206,4 +334,5 @@ Security note:
|
|
|
206
334
|
- execution logs redact secret-like keys (token, authorization, apiKey, etc.)
|
|
207
335
|
- environment-derived secret values are masked in persisted logs
|
|
208
336
|
- when auth is enabled, pass Authorization: Bearer <token> (or x-api-key) for /api routes
|
|
337
|
+
- with SPELL_API_AUTH_KEYS, non-admin list requests are restricted to their own tenant and cross-tenant tenant_id filters return 403 (TENANT_FORBIDDEN)
|
|
209
338
|
- do not set both SPELL_API_AUTH_KEYS and SPELL_API_AUTH_TOKENS at the same time
|