skillscript-runtime 0.24.0 → 0.25.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/UPGRADING.md +9 -1
  4. package/dist/bootstrap.d.ts +10 -0
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +7 -0
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/cli.js +4 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/connectors/types.d.ts +8 -0
  11. package/dist/connectors/types.d.ts.map +1 -1
  12. package/dist/connectors/types.js.map +1 -1
  13. package/dist/help-content.d.ts.map +1 -1
  14. package/dist/help-content.js +3 -0
  15. package/dist/help-content.js.map +1 -1
  16. package/dist/lint.d.ts +1 -1
  17. package/dist/lint.d.ts.map +1 -1
  18. package/dist/lint.js +145 -0
  19. package/dist/lint.js.map +1 -1
  20. package/dist/mcp-server.d.ts +8 -0
  21. package/dist/mcp-server.d.ts.map +1 -1
  22. package/dist/mcp-server.js +2 -0
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/parser.d.ts +9 -0
  25. package/dist/parser.d.ts.map +1 -1
  26. package/dist/parser.js +22 -6
  27. package/dist/parser.js.map +1 -1
  28. package/dist/runtime.d.ts +20 -0
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +147 -24
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/scheduler.d.ts +8 -0
  33. package/dist/scheduler.d.ts.map +1 -1
  34. package/dist/scheduler.js +3 -0
  35. package/dist/scheduler.js.map +1 -1
  36. package/dist/secrets.d.ts +119 -0
  37. package/dist/secrets.d.ts.map +1 -0
  38. package/dist/secrets.js +153 -0
  39. package/dist/secrets.js.map +1 -0
  40. package/dist/skill-catalog.d.ts.map +1 -1
  41. package/dist/skill-catalog.js +2 -0
  42. package/dist/skill-catalog.js.map +1 -1
  43. package/docs/adopter-playbook.md +41 -0
  44. package/docs/language-reference.md +82 -5
  45. package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
  46. package/package.json +1 -1
  47. package/scaffold/.env.example +13 -0
@@ -511,7 +511,7 @@ $ amp.amp_olsen_task task_type="scan" timeout=120 -> DISPATCH
511
511
  $ data_write content="${REPORT}" tags=["oncall"] approved="morning roundup" -> ACK
512
512
  ```
513
513
 
514
- **`(fallback: "value")` trailer** — Fires on dispatch throw OR empty bound value (empty string after trim, empty array, null/undefined). Honored on `$` dispatch and on `shell()` (fires on shell op throw or empty stdout). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted. A fired fallback is recorded in `result.fallbacks[]`.
514
+ **`(fallback: "value")` trailer** — Fires on dispatch throw (including a `timeout=N` expiry) OR empty bound value (empty string after trim, empty array, null/undefined). Honored on `$` dispatch and on `shell()` (fires on shell op throw or empty stdout). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted. A fired fallback is recorded in `result.fallbacks[]`.
515
515
 
516
516
  Note: an envelope object like `{items: []}` is a non-empty object and does NOT trigger the fallback even though its contained array is empty. To handle envelope-empty downstream, test the contained collection (`if ${R.items|length} == "0":`) or apply a filter (`${R.items|fallback:[]}`).
517
517
 
@@ -541,11 +541,11 @@ Routes through the wired LocalModel.
541
541
 
542
542
  | Kwarg | Required | Notes |
543
543
  |---|---|---|
544
- | `prompt="..."` | yes | Non-empty string. Substitution-resolved at runtime. |
545
- | `maxTokens=N` | no | Positive integer (number or numeric string). Forwarded as `runOpts.maxTokens`. |
544
+ | `prompt="..."` | yes | Non-empty string (empty/missing throws). Substitution-resolved at runtime. Must be a kwarg — there is no bare-string positional form (`$ llm "..."` is invalid; see the all-kwargs rule at the top of this section). |
545
+ | `maxTokens=N` | no | Positive integer (number or numeric string). Forwarded as `runOpts.maxTokens`. **camelCase — it's `maxTokens`, NOT `max_tokens`; the snake_case form is an unknown kwarg the bridge silently drops, caught by the `unknown-llm-arg` lint.** |
546
546
  | `model="X"` | no | Per-call model selection. Resolves against registered LocalModel aliases via `registry.getLocalModel(X)`; falls through to the default LocalModel with `model=X` passed as an upstream hint (e.g., Ollama tag) when X doesn't resolve. See `unknown-llm-model` lint below. |
547
547
 
548
- Plus the universal op-level kwargs (`timeout`, `approved`, `(fallback:)`, `-> R`).
548
+ Plus the universal op-level kwargs (`timeout`, `approved`, `(fallback:)`, `-> R`). `timeout` is runtime-enforced (popped before the connector sees it); a timeout expiry is a throw, so a `(fallback:)` trailer catches it.
549
549
 
550
550
  Canonical shape:
551
551
 
@@ -795,6 +795,83 @@ Missing-ref in the RHS produces a tier-1 runtime error.
795
795
  - `foreach IDENT in EXPR:` iterator vars are loop-local — `$set` bindings inside the loop don't persist after the loop ends
796
796
  - Target outputs (`${target.output}`) are accessible after the target completes
797
797
 
798
+ ## Secrets — secret.NAME references, {{secret.NAME}} sink markers, SKILLSCRIPT_SECRET_ provisioning, use-only enforcement
799
+
800
+ ## Secrets
801
+
802
+ Skills routinely need a credential — an API token, a deploy key — to reach an external service. Skillscript handles secrets **by reference, never by value**. A skill declares which secrets it may use and marks exactly where each is injected, but it can never read, bind, or print the value. The runtime resolves a secret only at the instant it is handed to a *sink* (a `shell` op or a `$ connector.tool` dispatch).
803
+
804
+ This is what lets a human approve a skill knowing the full set of credentials it can reach — and lets an agent-authored skill run without ever exposing the key to the agent. The author holds a *use*-capability (invoke a skill that reaches the key) without the *access*-capability (read the key).
805
+
806
+ ### Three steps
807
+
808
+ **1. Declare it in frontmatter.** List every secret the skill may reach. An approver reading the skill sees the whole credential surface up front.
809
+
810
+ ```
811
+ # Skill: send-email
812
+ # Requires: secret.AGENTMAIL_KEY
813
+ # Status: Draft
814
+ ```
815
+
816
+ **2. Place it with a `{{secret.NAME}}` marker — only inside a sink.** A secret may appear only where it is *used*: inside a `shell(...)` op or a `$ connector.tool` dispatch.
817
+
818
+ ```
819
+ run:
820
+ shell(command="curl -H 'Authorization: Bearer {{secret.AGENTMAIL_KEY}}' https://api.agentmail.to/...") -> R
821
+ ```
822
+
823
+ …or into a connector argument:
824
+
825
+ ```
826
+ $ http.post url="https://api..." authorization={{secret.AGENTMAIL_KEY}} -> R
827
+ ```
828
+
829
+ **3. Provision it as an env var**, named with the `SKILLSCRIPT_SECRET_` prefix:
830
+
831
+ ```
832
+ SKILLSCRIPT_SECRET_AGENTMAIL_KEY=am_us_...
833
+ ```
834
+
835
+ ### Semantics — the rules that matter
836
+
837
+ - **`{{secret.NAME}}` is not `${VAR}`.** `${VAR}` is readable substitution in the skill's variable scope — it can be bound, emitted, branched on, and it appears in the execution trace. A secret can do none of that. `${VAR}` cannot reach a secret; a `{{secret.NAME}}` marker is never readable as a var.
838
+ - **The name is a compile-time literal.** `${VAR}` substitution never reaches inside a `{{secret.…}}` marker (markers are opaque to substitution), and the runtime resolves a marker only against the declared `# Requires` set by exact-literal match. You cannot build a secret name dynamically.
839
+ - **Use-only, resolved at the sink.** The value is injected at the moment the shell op spawns or the connector dispatches — never bound to a variable, never emitted, never written to a trace. The trace records the *marker* (`{{secret.AGENTMAIL_KEY}}`), not the value. The binary/allowlist gate runs *before* resolution, so a refused op never even resolves the secret, and error/trace output is redacted to the marker form.
840
+ - **Misuse is a compile-time error (tier-1), not a runtime surprise:**
841
+ - `{{secret.NAME}}` in `emit`, `$set`, a condition, an `# Output:` template, a `file_*` / `notify` op, or a `(fallback: ...)` → **`secret-use-only`**.
842
+ - A `{{secret.NAME}}` marker with no matching `# Requires: secret.NAME` declaration → **`secret-undeclared`**.
843
+ - A marker whose name isn't a clean literal (e.g. `{{secret.${NM}}}`) → **`secret-dynamic-name`**.
844
+
845
+ ### Worked example
846
+
847
+ A skill that checks an inbox over a REST API. The key is declared once and used once, at the curl sink — it never touches a variable, the output, or the trace:
848
+
849
+ ```
850
+ # Skill: inbox-check
851
+ # Requires: secret.AGENTMAIL_KEY
852
+ # Returns: COUNT
853
+
854
+ check:
855
+ shell(argv=["curl","-s","-H","Authorization: Bearer {{secret.AGENTMAIL_KEY}}","https://api.agentmail.to/v0/inboxes/me/messages?labels=unread"]) -> RAW (fallback: "{\"count\":0}")
856
+ $ json_parse ${RAW} -> P
857
+ $set COUNT = ${P.count}
858
+ emit(text="Unread: ${COUNT}")
859
+
860
+ default: check
861
+ ```
862
+
863
+ ### Threat-model boundary — what this does NOT protect
864
+
865
+ The guarantees above stop *accidental* leakage and *undeclared* access: a secret is never bound, emitted, traced, leaked through a refused op, or reachable via a dynamically-built or undeclared name. They do **not** stop a *malicious skill author*. A skill authorized to use a secret in a **shell** sink can also make that sink exfiltrate it — e.g. `shell(command="printf %s {{secret.FLAG}}") -> OUT` then `emit(${OUT})` launders the secret through the skill's own stdout, and a `curl` could send it anywhere. This is inherent: once a command may *use* a secret, it may *misuse* it.
866
+
867
+ The controls for that boundary are:
868
+ - **Human approval review** — an approver who sees `printf {{secret}}` followed by `emit` rejects the skill. The `# Requires:` line + the visible sink are exactly what makes this reviewable.
869
+ - **Prefer connector sinks over raw shell.** A connector holds the credential out-of-band and applies it inside the dispatch, so the secret never round-trips through skill-visible output (`$ http.post ... authorization={{secret.NAME}}` returns the API's *data*, not the key). Use a `shell` secret only when no connector is available, and review such skills accordingly.
870
+
871
+ ### Why this shape
872
+
873
+ The marker/var split *is* the security model: the runtime is the reference monitor, the human approval is the grant, and the `# Requires:` line is the auditable statement of reach. Capability without access — the agent can act through the credential without ever holding it.
874
+
798
875
  ## Pipe filters — url, shell, json, trim, fallback, isodate
799
876
 
800
877
  Pipe filters apply transforms to resolved variables before substitution. Syntax: `${VAR|filter}` or `${VAR|filter:"arg"}` for parameterized filters. Filters operate at compile time for static values; for runtime-bound variables, filters apply at substitution time.
@@ -2479,5 +2556,5 @@ When any of these primitives ship, the relevant grammar moves into its canonical
2479
2556
 
2480
2557
  ---
2481
2558
 
2482
- *Rendered from `skillscript/skillscript-language-reference` — 2026-06-16 15:46 EDT*
2559
+ *Rendered from `skillscript/skillscript-language-reference` — 2026-06-28 15:20 EDT*
2483
2560
  *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-06-25T13:19:55.700Z",
5
+ "compiled_at": "2026-06-28T19:30:48.982Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscript-runtime",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
5
5
  "license": "MIT",
6
6
  "author": "Scott Shwarts <scotts@pobox.com>",
@@ -56,6 +56,19 @@
56
56
  # SKILLSCRIPT_SHELL_ALLOWLIST=curl,git,jq,grep
57
57
 
58
58
 
59
+ # ─── Secrets ───────────────────────────────────────────────────────────
60
+
61
+ # Provision a secret a skill can USE but never READ. A skill declares it with
62
+ # `# Requires: secret.NAME` and places it with a `{{secret.NAME}}` marker; the
63
+ # runtime injects the value only at a sink (a shell() op or a `$ connector.tool`
64
+ # dispatch) and never lets it reach an emit, a $set, a trace, or any readable
65
+ # surface. Name each secret SKILLSCRIPT_SECRET_<NAME> — the prefix scopes which
66
+ # env is secret-reachable (so {{secret.PATH}} can't pull $PATH). Operator-only:
67
+ # a skill author references a secret by name but cannot set or read it.
68
+ # SKILLSCRIPT_SECRET_AGENTMAIL_KEY=sk-live-...
69
+ # SKILLSCRIPT_SECRET_GITHUB_TOKEN=ghp_...
70
+
71
+
59
72
  # ─── Network / bind config ─────────────────────────────────────────────
60
73
 
61
74
  # HTTP bind address. Default 127.0.0.1 (localhost-only). Set 0.0.0.0 for