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.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -1
- package/UPGRADING.md +9 -1
- package/dist/bootstrap.d.ts +10 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +7 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/types.js.map +1 -1
- package/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +3 -0
- package/dist/help-content.js.map +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +145 -0
- package/dist/lint.js.map +1 -1
- package/dist/mcp-server.d.ts +8 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +22 -6
- package/dist/parser.js.map +1 -1
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +147 -24
- package/dist/runtime.js.map +1 -1
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +3 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/secrets.d.ts +119 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +153 -0
- package/dist/secrets.js.map +1 -0
- package/dist/skill-catalog.d.ts.map +1 -1
- package/dist/skill-catalog.js +2 -0
- package/dist/skill-catalog.js.map +1 -1
- package/docs/adopter-playbook.md +41 -0
- package/docs/language-reference.md +82 -5
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/package.json +1 -1
- 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-
|
|
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")`)*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillscript-runtime",
|
|
3
|
-
"version": "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>",
|
package/scaffold/.env.example
CHANGED
|
@@ -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
|