skillscript-runtime 0.19.16 → 0.20.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/README.md +11 -1
- package/dist/approval.d.ts +80 -54
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +156 -120
- package/dist/approval.js.map +1 -1
- package/dist/bootstrap.d.ts +41 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +59 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +245 -24
- package/dist/cli.js.map +1 -1
- package/dist/compile.js +7 -0
- package/dist/compile.js.map +1 -1
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +13 -1
- package/dist/composition.js.map +1 -1
- package/dist/connectors/skill-store.d.ts.map +1 -1
- package/dist/connectors/skill-store.js +41 -13
- package/dist/connectors/skill-store.js.map +1 -1
- package/dist/connectors/sqlite-skill-store.d.ts.map +1 -1
- package/dist/connectors/sqlite-skill-store.js +21 -12
- package/dist/connectors/sqlite-skill-store.js.map +1 -1
- package/dist/dashboard/spa/app.js +186 -8
- package/dist/dashboard/spa/index.html +2 -0
- package/dist/dashboard/spa/styles.css +42 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +24 -7
- package/dist/lint.js.map +1 -1
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +19 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/parser.js +2 -2
- package/dist/parser.js.map +1 -1
- package/dist/runtime-config.d.ts +6 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +8 -0
- package/dist/runtime-config.js.map +1 -1
- package/dist/runtime-env-resolver.d.ts +4 -0
- package/dist/runtime-env-resolver.d.ts.map +1 -1
- package/dist/runtime-env-resolver.js +27 -0
- package/dist/runtime-env-resolver.js.map +1 -1
- package/dist/runtime.d.ts +18 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +84 -17
- package/dist/runtime.js.map +1 -1
- package/dist/safe-path.d.ts +22 -0
- package/dist/safe-path.d.ts.map +1 -1
- package/dist/safe-path.js +72 -1
- package/dist/safe-path.js.map +1 -1
- package/dist/scheduler.d.ts +3 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +8 -0
- package/dist/scheduler.js.map +1 -1
- package/docs/adopter-playbook.md +75 -25
- package/docs/configuration.md +5 -1
- package/docs/connector-contract-reference.md +5 -1
- package/docs/language-reference.md +83 -130
- package/docs/sqlite-skill-store.md +5 -1
- package/examples/skillscripts/classify-support-ticket.skill.md +1 -1
- package/examples/skillscripts/data-store-roundtrip.skill.md +1 -1
- package/examples/skillscripts/doc-qa-with-citations.skill.md +1 -1
- package/examples/skillscripts/feedback-sentiment-scan.skill.md +1 -1
- package/examples/skillscripts/hello-world.skill.md +1 -1
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/examples/skillscripts/morning-brief.skill.md +1 -1
- package/examples/skillscripts/queue-length-monitor.skill.md +1 -1
- package/examples/skillscripts/service-health-watch.skill.md +1 -1
- package/examples/skillscripts/skill-store-roundtrip.skill.md +1 -1
- package/examples/skillscripts/youtrack-morning-sweep.skill.md +1 -1
- package/package.json +1 -1
- package/scaffold/skills/data-store-roundtrip.skill.md +1 -1
- package/scaffold/skills/hello-world.skill.md +1 -1
- package/scaffold/skills/skill-store-roundtrip.skill.md +1 -1
package/docs/adopter-playbook.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Adopter Playbook
|
|
3
|
+
description: "Stand up the runtime, wire your substrates, supervise it as a service, and point your agent at it over MCP."
|
|
4
|
+
mode: wide
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
How to wire skillscript-runtime into your deployment. Written for Joe-Programmer: you have your own substrate stack (data store, agent harness, LLM endpoint, filesystem), and you want skillscript to slot in rather than dictate.
|
|
4
8
|
|
|
@@ -204,8 +208,10 @@ Create `skillscript.config.json` in your `$SKILLSCRIPT_HOME`:
|
|
|
204
208
|
|
|
205
209
|
**For custom substrates**: write your own bootstrap. See `examples/custom-bootstrap.example.ts` and `examples/onboarding-scaffold/bootstrap.ts` for complete worked walkthroughs.
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
Security knobs that adopters wiring real substrates should know about:
|
|
208
212
|
|
|
213
|
+
- **The approval boundary (secured mode).** `SKILLSCRIPT_SECURED_MODE=true` enforces that only key-signed skills perform effects. Default-deny by design — leave it off only for trusted-author / single-operator setups. Full detail, the approve flow, and key custody: [Approval + secured mode](#approval--secured-mode).
|
|
214
|
+
- **Filesystem path allowlist.** `SKILLSCRIPT_FS_ALLOWLIST` is default-deny — `file_read` / `file_write` refuse every path until you wire roots. See [Filesystem path allowlist](#filesystem-path-allowlist). (Keep your approval-key directory out of it.)
|
|
209
215
|
- **Per-connector tool allowlists** — `allowed_tools` on each `connectors.json` MCP connector entry restricts which tools that connector can dispatch. Three-state (`undefined` = allow all, `[]` = allow none, listed = exactly those). Tier-1 `disallowed-tool` lint + runtime defense-in-depth refuse out-of-list dispatch. **`allowed_tools` belongs at the entry top-level**, sibling to `class` and `config` — NOT inside the `config` block. The loader hard-errors on misplacement (placing it inside `config:` would silently allow-all every tool — the worst-case failure mode for a security control). See `docs/configuration.md` §"Named MCP connector instances" for the JSON shape.
|
|
210
216
|
- **Shell-execution discipline** — `shell(command="...")` runs structured-spawn by default (binary on PATH, whitespace-tokenized argv, no bash). `shell(command="...", unsafe=true)` opts into bash interpretation (pipes, `$VAR`, command substitution) and refuses to fire unless the runtime is configured with `enable_unsafe_shell = true` in `config.toml`. Lint flags every `unsafe=true` op tier-2 to keep audit posture visible. See `scaffold/config.toml` for the documented default + `help({topic:"lint-codes"})` for the `unsafe-shell-disabled` rule.
|
|
211
217
|
|
|
@@ -342,6 +348,49 @@ With the service running, point your agent at the runtime's `/rpc` endpoint.
|
|
|
342
348
|
- **Port matches what the runtime binds.** Default `7878` (`SKILLSCRIPT_PORT` env var / `dashboard.port` config). Two-instance setups (dev + adopter side-by-side per §5) need distinct ports — e.g., adopter on `7879`. Whatever the supervisor unit specifies is what the agent config must point at.
|
|
343
349
|
- **Auth + bind address.** `/rpc` has no auth and binds to `127.0.0.1` by default — the right posture for a local agent talking to a local runtime. If you set `SKILLSCRIPT_HOST=0.0.0.0` to reach the runtime across the network, front it with your own auth layer (reverse proxy + bearer token, mTLS, etc.) — same posture as the `/event` HTTP ingress, which has its own bearer-token gate (`SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN`).
|
|
344
350
|
|
|
351
|
+
## Approval + secured mode
|
|
352
|
+
|
|
353
|
+
Every skill carries a lifecycle status — `Draft → Approved → Disabled`. **Only `Approved` skills execute**; `Draft` is the safety gate (a skill under authoring or review), `Disabled` is retired. That status gate applies in *both* runtime modes. What the mode decides is whether an `Approved` skill must additionally be **cryptographically keyed**:
|
|
354
|
+
|
|
355
|
+
| | Unsecured | Secured (`SKILLSCRIPT_SECURED_MODE=true`) |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `Draft` skill | refused | refused |
|
|
358
|
+
| `Approved` skill | runs — a bare `# Status: Approved` is sufficient (unkeyed) | runs **only** if the body carries a valid operator signature |
|
|
359
|
+
| Tamper-evidence | none (an Approved body can be edited freely) | yes — editing the body breaks the signature |
|
|
360
|
+
| Effectful ops (`$`, `shell`, `file_*`, `notify`) | run | **refused unless the skill is signed** (the `effectsAuthorized` capability gate) |
|
|
361
|
+
| Who can grant approval | anyone who can set the status | only a holder of the operator's private key |
|
|
362
|
+
|
|
363
|
+
**Unsecured** is the trusted-author / single-operator convenience posture: you trust whoever writes skills, the status flag is the only barrier. **Secured** is the enforced boundary — an unapproved or tampered skill cannot perform a single effectful op, regardless of how it's dispatched (CLI, cron, `/event`, MCP, in-skill composition). Unsecured is the current default; secured is the recommended posture for any multi-author or networked deployment.
|
|
364
|
+
|
|
365
|
+
> The shell + filesystem allowlists (below) are **always enforced, in both modes** — they bound *what* a running skill may touch. Secured mode adds the orthogonal boundary on *whether an unapproved skill runs at all*.
|
|
366
|
+
|
|
367
|
+
### Asymmetric signing — how secured approval works
|
|
368
|
+
|
|
369
|
+
Approval is an Ed25519 signature, not a shared secret:
|
|
370
|
+
|
|
371
|
+
- **Private key** — held by the operator, read **only** by the approve flow (`skillfile approve`). The runtime never loads it. For full isolation, run the runtime under a uid that cannot read the private key — then a co-resident agent can't forge approvals even with full read access to the runtime process. Same-uid still works, but isn't fully isolated (a documented 1.0 precondition; managed custody closes it later).
|
|
372
|
+
- **Public key** — non-secret; the runtime reads it to *verify* signatures on every execution. No hot-path secret is the point: verification needs only the public key.
|
|
373
|
+
|
|
374
|
+
First-run provisioning is automatic — starting the runtime or `skillfile init` in secured mode generates the keypair if absent (private written `0600`, default `~/.config/skillscript/approval.{key,pub}`, deliberately outside `SKILLSCRIPT_HOME`, the agent-readable data dir).
|
|
375
|
+
|
|
376
|
+
### The approve flow
|
|
377
|
+
|
|
378
|
+
| Action | How |
|
|
379
|
+
|---|---|
|
|
380
|
+
| Approve one skill (review the body, then sign) | `skillfile approve <name>` |
|
|
381
|
+
| Batch-approve everything pending | `skillfile reapprove --apply` (dry-run preview: `skillfile reapprove`) |
|
|
382
|
+
| Approve from the dashboard | the **Approvals** queue — review the body + security signals, then run the emitted command at your terminal |
|
|
383
|
+
|
|
384
|
+
The dashboard is a **review** surface, not a signing surface: it surfaces the pending queue and per-skill security-signal summary, but signing happens at your terminal where the private key lives (the network-facing runtime never holds it). `skill_write` / `skill_status` **cannot** grant approval in secured mode — an agent-written or status-flipped skill lands `Draft` until a key-holder signs it. (In unsecured mode, `skill_status → Approved` is honored directly, unkeyed.)
|
|
385
|
+
|
|
386
|
+
### Migrating an existing skill set
|
|
387
|
+
|
|
388
|
+
Turning secured mode on means any skill that's `Approved` without a valid signature is refused at execution. `skillfile reapprove` sweeps the store, reports the set needing re-blessing, and `--apply` re-signs them in one pass — so you don't run `skillfile approve` once per skill.
|
|
389
|
+
|
|
390
|
+
### Bundled demos
|
|
391
|
+
|
|
392
|
+
Bundled example skills ship as `# Status: Draft` — a signature baked at package-build time could never validate on your install (the key is per-operator). `skillfile init` locally approves the three seeded demos with *your* machine's authority (secured → provision keypair + sign; unsecured → bare Approved), so they're runnable immediately after init.
|
|
393
|
+
|
|
345
394
|
## Shell binary allowlist
|
|
346
395
|
|
|
347
396
|
**The runtime enforces a default-deny operator allowlist for binaries reachable via `shell(...)` ops.** Skill authors are agents, agents are a weak trust anchor (hallucination, prompt-injection, no human-in-loop at scale), and operator-side scoping converts "a human reviews every skill" from discipline into an enforced constraint at the language level.
|
|
@@ -502,6 +551,25 @@ Per-skill capability declaration: skills declare what shell binaries they need i
|
|
|
502
551
|
|
|
503
552
|
The operator policy validates `declared ∩ allowlist` — each skill's shell footprint becomes self-documenting and auditable. Slated for a future ring once the chokepoint + observability surfaces ship.
|
|
504
553
|
|
|
554
|
+
## Filesystem path allowlist
|
|
555
|
+
|
|
556
|
+
**The runtime enforces a default-deny operator allowlist for paths reachable via `file_read(...)` / `file_write(...)` ops** — the third operator boundary, mirroring the shell allowlist with the same rationale (skill authors are agents; the operator scopes which parts of the filesystem skills may touch).
|
|
557
|
+
|
|
558
|
+
| Operator switch | Controls |
|
|
559
|
+
|---|---|
|
|
560
|
+
| `SKILLSCRIPT_FS_ALLOWLIST` | Comma-separated roots under which `file_read` / `file_write` may operate |
|
|
561
|
+
|
|
562
|
+
- **Default-deny.** An unset or empty allowlist refuses *every* file op with `FilePathNotAllowedError` — a freshly-installed runtime does no file I/O until you wire roots. Applies to `file_read` as well as `file_write` (a read-then-`emit` is an exfiltration path).
|
|
563
|
+
- **Canonicalized before the check.** Both the target and each allowed root are resolved to their real absolute path (realpath, component-by-component), so `..` traversal and symlink evasion can't escape an allowed root — the classic allowlist bypasses are closed.
|
|
564
|
+
- **Off-allowlist is final.** No in-skill keyword (`approved=`, `# Autonomous: true`) escapes it. **Keep secret / key directories OUT of the allowlist** — a skill must never be able to read the operator's approval key. (This is why the default key path lives outside `SKILLSCRIPT_HOME`.)
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
# permit reads/writes under a workspace + an event-drop dir
|
|
568
|
+
SKILLSCRIPT_FS_ALLOWLIST=/srv/skillscript/workspace,/var/skillscript/events
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
TOCTOU note: the check resolves the real path at call time; a symlink swapped between check and open is a residual closed by fd-based opens later. Checking the resolved real path is the standard mitigation shipped today.
|
|
572
|
+
|
|
505
573
|
## Trigger model
|
|
506
574
|
|
|
507
575
|
**The trigger surface is two primitives.**
|
|
@@ -711,7 +779,7 @@ A skill may carry template prose in one region. Splitting it into two — some p
|
|
|
711
779
|
|
|
712
780
|
- **`unset-template-var`** (tier-1) — every `${VAR}` in the template must resolve to a `# Vars:` / `# Requires:` input, an ambient ref (`NOW`, `USER`, ...), or a `$set` / `->` binding somewhere in the skill body. Tier-1 because an unbound ref renders empty silently.
|
|
713
781
|
- **`template-looks-like-target`** (tier-2) — bare `<word>:` alone in the template region, no following op-block. Ambiguous shape — author may have meant a target.
|
|
714
|
-
- **`connector-as-tool`** (tier-1) — `$ <connector> <tool>` space-separated catches the muscle-memory foot-gun (CLI-style `git status`). Bare-form dispatch treats the first token as the tool name, sending `name: "<connector>"` to the MCP server, which replies with a misdirecting
|
|
782
|
+
- **`connector-as-tool`** (tier-1) — `$ <connector> <tool>` space-separated catches the muscle-memory foot-gun (CLI-style `git status`). Bare-form dispatch treats the first token as the tool name, sending `name: "<connector>"` to the MCP server, which replies with a misdirecting `Tool '<connector>' not found.` The two correct shapes are `$ <connector>.<tool> args` (dotted) or `$ <tool> args` (bare-tool; runtime resolves).
|
|
715
783
|
- **`remote-result-needs-parse`** (tier-3) — `${R|length}` on an `R` bound by `$` dispatch. Per-MCP-server result shapes vary: if the server returns prose-wrapped or non-JSON text, the value binds as a STRING and `|length` returns the string's char-count instead of element-count. Add `$ json_parse ${R} -> P` after the dispatch and use `${P|length}`. Suppressed when the skill already does `$ json_parse ${R}` somewhere — your defensive parse is taken as intent.
|
|
716
784
|
- **`body-template-detected`** (tier-3) — non-blank, non-`#` lines in the body region, no `${...}` interpolations, no text-consuming `# Output:` declaration. Suggests "I wrote prose; it became template by accident." Prefix with `#` to mark as comments, or add an interpolation / `# Output:` to confirm intent.
|
|
717
785
|
- **`emit-with-template`** (tier-3) — skill has both a template AND `emit(text=...)` calls. Confirms the channel-shift is intentional.
|
|
@@ -860,7 +928,7 @@ Callers reading `WakeReceipt.woken` distinguish "the substrate woke them" from "
|
|
|
860
928
|
|
|
861
929
|
**Pattern 3 — session echo on receipts.** When your substrate routes to a specific session, echo it back on `DeliveryReceipt.session_id` / `WakeReceipt.session_id`. Dashboards rendering "delivered to alice@laptop-tab-3" rather than just "delivered to alice" depend on this.
|
|
862
930
|
|
|
863
|
-
**Pattern 4 — read `meta.origin.caller_agent_id` to attribute, not for scope.** The `DeliveryMeta` envelope your `deliver()` receives carries `origin.caller_agent_id` = the *authenticated caller* who fired the dispatch (not the skill's owner — those are separate semantics; see [Connector Contract Reference](connector-contract-reference.md) §field semantics). Use it for *attribution* — rendering
|
|
931
|
+
**Pattern 4 — read `meta.origin.caller_agent_id` to attribute, not for scope.** The `DeliveryMeta` envelope your `deliver()` receives carries `origin.caller_agent_id` = the *authenticated caller* who fired the dispatch (not the skill's owner — those are separate semantics; see [Connector Contract Reference](connector-contract-reference.md) §field semantics). Use it for *attribution* — rendering `from <caller>` on the receiving end, audit logs, accountability — not for authorization scoping. Outbound substrate scoping should derive from the *skill owner* (which the runtime applies at the connector layer via `ctx.agentId`, not via the envelope). If `caller_agent_id` is undefined on a delivery you receive, it means the chain originated from a non-human trigger (cron / event / scheduler) — your substrate should attribute it as "system-fired" or similar, not assume an identity.
|
|
864
932
|
|
|
865
933
|
**Pattern 5 — surface non-fatal notes via `DeliveryReceipt.warnings`.** When your substrate needs to signal something non-fatal about a delivery — "stripped @session because verb is deliver", "rate-limit hint", "fan-out: delivered to 3 sessions" — return them as `warnings: string[]` on the receipt instead of writing to stderr. The runtime echoes warnings onto `AgentDeliveryReceiptRecord.receipt.warnings`, where the dashboard can render them and observability tools can scrape them. Stderr noise gets lost; receipt warnings are structured + caller-visible.
|
|
866
934
|
|
|
@@ -887,12 +955,7 @@ How `author` is captured depends on how the skill gets written:
|
|
|
887
955
|
|
|
888
956
|
Adopters whose `SkillStore` is backed by an addressable substrate (e.g., a memory store) can author skills by writing the substrate record directly — without going through the MCP `skill_write` handler. This captures `SkillMeta.author` from the substrate's own writer-identity (whatever the direct-write API authenticates as).
|
|
889
957
|
|
|
890
|
-
**Gotcha:** direct-write
|
|
891
|
-
|
|
892
|
-
1. Write the skill with `# Status: Draft` via your substrate's direct-write API.
|
|
893
|
-
2. Call `skill_status({name, new_state: "Approved"})` via MCP (or the dashboard's Approve button). This stamps the token and preserves the captured author.
|
|
894
|
-
|
|
895
|
-
Write-Approved-without-stamp will fail at execute time with `ApprovalRejectedError`. Always Draft-then-promote.
|
|
958
|
+
**Gotcha:** in **secured mode**, a direct-write declaring `# Status: Approved` without a valid signature is forced to `Draft` — the substrate can't mint approval, only the operator's key can. Publish by writing `Draft`, then signing via `skillfile approve <name>` or the dashboard Approvals queue (both preserve the captured author). In **unsecured mode** a bare `# Status: Approved` direct-write is honored as-is. Either way, in-skill `$ skill_write` always lands its child `Draft` regardless of body declaration. See [Approval + secured mode](#approval--secured-mode).
|
|
896
959
|
|
|
897
960
|
## Identity propagation — for multi-agent hosts
|
|
898
961
|
|
|
@@ -935,7 +998,7 @@ X-Agent-Id: alice
|
|
|
935
998
|
|
|
936
999
|
### Trust model
|
|
937
1000
|
|
|
938
|
-
The runtime trusts the host's header attestation. There's no signature verification — anyone reaching the runtime with a forged `X-Agent-Id` could claim to be anyone. The runtime is **not** the authentication boundary; the host is. Bilateral trust:
|
|
1001
|
+
The runtime trusts the host's header attestation. There's no signature verification on the *identity claim* (distinct from secured-mode approval, which *is* signature-verified — different boundary, different mechanism): anyone reaching the runtime with a forged `X-Agent-Id` could claim to be anyone. The runtime is **not** the authentication boundary; the host is. Bilateral trust:
|
|
939
1002
|
|
|
940
1003
|
- **The host** (your MCP gateway) authenticates the agent via its own auth surface (OAuth, JWT, session cookies, mTLS — whatever fits your platform) and injects the verified identity into the outbound `X-Agent-Id` header.
|
|
941
1004
|
- **The runtime** trusts the host because you configured it to (`mcpCallerIdentityHeader` is opt-in; unset means "I don't trust any inbound identity claim, fall back to my own writer identity").
|
|
@@ -1033,20 +1096,7 @@ See `examples/custom-bootstrap.example.ts` for a worked walkthrough.
|
|
|
1033
1096
|
- **Durable-forever opt-in via `expires_at: null`.** `DataWrite.expires_at` accepts a unix timestamp for finite expiry, `null` to opt into "durable forever" (the portable verb for substrates with default TTL — AMP memory vaults, Redis with default expiry, hosted memory APIs), or omitted (substrate's default lifecycle, may be durable or may have decay). Substrates that are durable-by-default (the bundled `SqliteDataStore`) treat `null` as a no-op. Substrates with default sweep should map `null` to their pin / no-decay flag.
|
|
1034
1097
|
- **Two trigger primitives, both functional.** `cron` (time-based) and `event` (HTTP `/event` ingress, named registration). All other concepts that look like triggers — session-start, agent-event, file-watch, sensor — are adapter responsibilities: the adapter POSTs `/event` when relevant. Keeps the runtime substrate-neutral; the trigger surface stays tight.
|
|
1035
1098
|
- **Output kinds are intentionally substrate-neutral.** `# Output:` accepts `text` / `agent: <name>` / `template: <name>` / `file: <path>` / `none`. Substrate-specific values (`slack:`, `card:`, etc.) are out of scope — adopters wanting Slack / WhatsApp / Discord / etc. delivery use either `$ slack.post ...` MCP dispatch inside the skill body OR deliver via `agent: <name>` and let the receiving agent decide.
|
|
1036
|
-
- **Authorization is
|
|
1037
|
-
|
|
1038
|
-
```ts
|
|
1039
|
-
import { registerApprovalFn, setPreferredApprovalVersion } from "skillscript-runtime";
|
|
1040
|
-
import { createHmac } from "node:crypto";
|
|
1041
|
-
|
|
1042
|
-
// v2: HMAC-SHA256 with operator-held key. Agent that knows the algorithm
|
|
1043
|
-
// can no longer self-stamp without the key.
|
|
1044
|
-
const key = process.env["APPROVAL_HMAC_KEY"]!;
|
|
1045
|
-
registerApprovalFn("v2", (body) => createHmac("sha256", key).update(body).digest("hex"));
|
|
1046
|
-
setPreferredApprovalVersion("v2"); // dashboard now stamps v2 on Approve clicks
|
|
1047
|
-
```
|
|
1048
|
-
|
|
1049
|
-
Wire this in your bootstrap BEFORE any skill is stamped — otherwise existing skills carry `v1:` tokens that still verify (CRC32 stays registered) but new approvals use the upgraded function. The runtime maintains a per-version registry, so mixed-version skill bodies coexist cleanly.
|
|
1099
|
+
- **Authorization is signature-based in secured mode.** An `Approved` skill runs unkeyed in unsecured mode (a bare `# Status: Approved` is sufficient) and requires a valid Ed25519 operator signature in secured mode — verified on every execution against the operator's public key, with the private key held off the runtime. See [Approval + secured mode](#approval--secured-mode) for the model, the approve flow, key custody, and migration. The shell + filesystem allowlists bound blast radius in *both* modes; secured mode is what gates whether an unapproved skill runs at all.
|
|
1050
1100
|
|
|
1051
1101
|
## Skill discovery + cross-agent composition
|
|
1052
1102
|
|
package/docs/configuration.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Configuration
|
|
3
|
+
description: "How to configure a skillscript-runtime deployment — connectors.json substrate selection, named MCP connector wiring, runtime knobs, and adopter-custom impls."
|
|
4
|
+
mode: wide
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
How to configure a skillscript-runtime deployment.
|
|
4
8
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Connector Contract Reference
|
|
3
|
+
description: "The substrate-neutral contracts adopters implement to wire their own backends. Interface signatures, payload shapes, durability + identity conventions."
|
|
4
|
+
mode: wide
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
The substrate-neutral contracts skillscript-runtime exposes for adopters to wire their own substrate behind. This doc is the **canonical source of truth** for the AgentConnector contract. The interface shape was locked at v1.0 by the v0.9.6 audit (thread `b722bbf4`); the wake/deliver receipt shapes carry post-lock refinements per the v0.18.2 session-targeting + graceful-degradation requirements.
|
|
4
8
|
|
|
@@ -1,91 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Language Reference
|
|
3
|
+
description: "Canonical spec — syntax, ops, semantics. Frontmatter, ops, filters, conditionals, triggers, output kinds, composition, error handling."
|
|
4
|
+
mode: wide
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
Canonical language reference for skillscript. Audience: skill authors (human + agent). Specifies what is valid syntax, what behavior to expect at compile + runtime, and what is currently pending implementation.
|
|
4
8
|
|
|
5
9
|
Implementation state is cross-referenced to commit hashes; pending items mark v2/v3 work.
|
|
6
10
|
|
|
7
11
|
|
|
8
|
-
## Not yet implemented, but planned
|
|
9
|
-
|
|
10
|
-
These are features designed or anticipated but not yet implemented in the current build. Authors should not use these forms; they will not compile.
|
|
11
|
-
|
|
12
|
-
## Control flow
|
|
13
|
-
|
|
14
|
-
- **`while CONDITION:` loops** — today's iteration is `foreach IDENT in EXPR:` only. While loops are planned for ad-hoc orchestration patterns ("loop until response contains 'done'").
|
|
15
|
-
- **Arithmetic in `$set`** — today accepts literals + `${VAR}` interpolation; no `+ - * /` operators. Planned alongside `while` for turn counters and orchestration bookkeeping.
|
|
16
|
-
|
|
17
|
-
## Pipe filter extensions
|
|
18
|
-
|
|
19
|
-
- **Array aggregation primitives** — `|max`, `|min`, `|sum`, `|reduce` over arrays. Today the language tops out at "shape one record" — aggregating across an array requires `foreach` accumulator ceremony. Planned as a design question: is `foreach` the deliberate ceiling for aggregation, or do we add primitives?
|
|
20
|
-
|
|
21
|
-
## Triggers — note
|
|
22
|
-
|
|
23
|
-
Trigger sources are `cron` + `event` only (see the Triggers section). There is no separate `agent-event:` / `file-watch:` / `sensor:` trigger source — anything that isn't time-based is an external adapter that POSTs to the `/event` ingress. So there's nothing pending here on the trigger axis; the entry remains only to point readers who expect those sources at the adapter-POST pattern.
|
|
24
|
-
|
|
25
|
-
## Synchronous agent exchange
|
|
26
|
-
|
|
27
|
-
- **`exchange()` runtime-intrinsic op** — synchronous send + wait pattern for multi-agent conferences. Awaits adopter-substrate queue impl + AgentConnector contract grow.
|
|
28
|
-
|
|
29
|
-
## Tests
|
|
30
|
-
|
|
31
|
-
- **`# Tests:` block** with `given:` / `expect:` assertions — author-authored test cases. Will land when adopter signal demands test infrastructure.
|
|
32
|
-
|
|
33
|
-
## Output kinds
|
|
34
|
-
|
|
35
|
-
- **`# Output: file: <path>`** — file-output routing parses but no router exists today.
|
|
36
|
-
- **`# Output: card:`** — depends on a substrate-side card render surface; not implemented.
|
|
37
|
-
|
|
38
|
-
## Persistent state with declared scope
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
$set NAME = value scope=skill-local
|
|
42
|
-
$set NAME = value scope=agent-global
|
|
43
|
-
$set NAME = value scope=session
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Scopes: skill-local (persists across fires of this skill, not visible to other skills), agent-global (visible to all skills of the same agent), session (alive for the duration of the current session, cleared at session end). Backed by a configured data-records connector.
|
|
47
|
-
|
|
48
|
-
## Sensors as a read-channel category
|
|
49
|
-
|
|
50
|
-
> `sensor:` is not a trigger source — to fire a skill on a sensor signal, a sensor adapter POSTs to `/event` (see Triggers). The concept below is the separate *read-channel* idea — continuous values an agent reads, distinct from triggering — which remains a future possibility if demand surfaces.
|
|
51
|
-
|
|
52
|
-
Distinct from triggers. Sensors are continuous channels the agent reads but doesn't emit on. Planned syntax:
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
# Sensors: presence, screen-state, voice-prosody
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Ambient refs `$(SENSOR.presence)`, `$(SENSOR.voice-prosody.affect)` for read access. Privacy-gating discipline determines when a sensor is readable.
|
|
59
|
-
|
|
60
|
-
## Time as first-class primitives
|
|
61
|
-
|
|
62
|
-
Currently `$(NOW)` (wall-clock). Planned relative-time primitives:
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
$(SECONDS_SINCE_LAST_USER_MESSAGE)
|
|
66
|
-
$(MINUTES_SINCE_SESSION_START)
|
|
67
|
-
$(SECONDS_SINCE_LAST_FIRE_OF.<skill-name>)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Most "right time" reasoning is relative, not wall-clock.
|
|
71
|
-
|
|
72
|
-
## Other planned
|
|
73
|
-
|
|
74
|
-
- **Absence-as-trigger** — `# Triggers: idle: 5m` fire-on-quiet primitive (would be a new trigger source beyond `cron`|`event`, or an adapter that POSTs on a timer)
|
|
75
|
-
- **Time-windowed aggregation** — filter-like primitives across firings (e.g., "user has shown frustration in 3 of 5 recent turns")
|
|
76
|
-
- **Debounce / rate-limit / coalesce** — declarative queueing policy headers
|
|
77
|
-
- **Suppression as valid output** — explicit "fire-and-suppress" (different from `# Output: none`)
|
|
78
|
-
- **Cross-skill pub-sub** — `# Publishes: signal.X` / `# Subscribes: signal.Y` decoupling
|
|
79
|
-
- **Confidence/threshold gating** — `# RequiresConfidence: classifier >= 0.8` / `# RequiresThreshold:`
|
|
80
|
-
- **Invocation-control axis** — `# Invocable-By: user | agent | trigger` (sensitive ops shouldn't leak across invocation boundaries)
|
|
81
|
-
- **Channel/locality awareness** — `$(CHANNEL_TYPE)`, `$(CHANNEL_PRIVACY)` ambient refs for routing decisions
|
|
82
|
-
- **Introspection primitives** — `$(PROMPT_CONTEXT.size)`, `$(SKILLS_FIRED_RECENTLY.last-1h)`, `$(SELF.confidence-trend)`
|
|
83
|
-
- **Capability declarations** — `# Requires-Capabilities: sensors=[mic, camera], tools=[...]` (audit surface for operators)
|
|
84
|
-
|
|
85
|
-
## When the language extends, this section shrinks
|
|
86
|
-
|
|
87
|
-
When any of these primitives ship, the relevant grammar moves into its canonical section (Ops reference, Variables, Triggers, etc.) and the entry here is removed. This section stays alive as a continuous staging area for the next horizon of unshipped work.
|
|
88
|
-
|
|
89
12
|
## Overview & language model
|
|
90
13
|
|
|
91
14
|
Skillscript is a constrained domain-specific language for authoring agent workflows. A skillscript is a declarative recipe: a small program with a dependency DAG of named targets, each composed of typed operations. Skillscripts are written once and executed many times.
|
|
@@ -378,7 +301,7 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
|
|
|
378
301
|
| `emit` | `emit(text="...")` | none | Append to the skill's emission stream; consumed by the configured `# Output:` delivery channel. |
|
|
379
302
|
| `notify` | `notify(agent="...", message="...", [event_type=...], [correlation_id=...]) -> ACK` | optional | Mid-skill agent alert; synchronous send via configured AgentConnector. |
|
|
380
303
|
| `inline` | `inline(skill="<data-skill-name>")` | none | Compile-time inline of an Approved `# Type: data` skill. Resolves at compile, records `content_hash` in provenance. |
|
|
381
|
-
| `execute_skill` | `execute_skill(skill_name="...", inputs
|
|
304
|
+
| `execute_skill` | `execute_skill(skill_name="...", inputs=\{...}) -> R` | optional | Composition primitive. Runtime-resolved. See Composition section. |
|
|
382
305
|
| `shell` | `shell(command="...") -> R` / `shell(argv=[...]) -> R` / `shell(command="...", unsafe=true) -> R` | optional | Structural spawn (default), explicit-argv spawn (`argv=[...]`, no tokenizer), or full-shell exec (`unsafe=true`, gated by `runtime.enable_unsafe_shell`). Binary gated by the operator allowlist (see below). stdout binds. |
|
|
383
306
|
| `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. |
|
|
384
307
|
| `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
|
|
@@ -735,12 +658,12 @@ deliver:
|
|
|
735
658
|
|
|
736
659
|
| Class | Op | Shape | Binding |
|
|
737
660
|
|---|---|---|---|
|
|
738
|
-
| Mutation | `$set` | `$set NAME = value` (with
|
|
661
|
+
| Mutation | `$set` | `$set NAME = value` (with `$\{VAR}` interpolation at bind) | NAME (no arrow) |
|
|
739
662
|
| Mutation | `$append` | `$append VAR <value>` (type-dispatched: list element / string concat) | VAR (no arrow) |
|
|
740
663
|
| Runtime-intrinsic | `emit` | `emit(text="...")` or `emit(text="""...""")` for multi-line | none |
|
|
741
664
|
| Runtime-intrinsic | `notify` | `notify(agent="...", [message=...], [event_type=...], [correlation_id=...]) -> ACK` | optional |
|
|
742
665
|
| Runtime-intrinsic | `inline` | `inline(skill="<name>")` | none (compile-time) |
|
|
743
|
-
| Runtime-intrinsic | `execute_skill` | `execute_skill(skill_name="...", inputs
|
|
666
|
+
| Runtime-intrinsic | `execute_skill` | `execute_skill(skill_name="...", inputs=\{...}) -> R` | optional |
|
|
744
667
|
| Runtime-intrinsic | `shell` | `shell(command="...", [unsafe=true], [approved="..."]) [-> R] [(fallback: "...")]` or `shell(argv=[...], [approved="..."]) [-> R] [(fallback: "...")]` (mutually exclusive forms; binary allowlist applies to both) | optional |
|
|
745
668
|
| Runtime-intrinsic | `file_read` | `file_read(path="...") -> R` | required |
|
|
746
669
|
| Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
|
|
@@ -769,13 +692,13 @@ Injected automatically at runtime; never declared by the author.
|
|
|
769
692
|
|
|
770
693
|
| Var | Value |
|
|
771
694
|
|-----|-------|
|
|
772
|
-
|
|
|
773
|
-
|
|
|
774
|
-
|
|
|
775
|
-
|
|
|
776
|
-
|
|
|
777
|
-
|
|
|
778
|
-
|
|
|
695
|
+
| `$\{NOW}` | ISO-8601 timestamp at op-dispatch time |
|
|
696
|
+
| `$\{USER}` | The configured user identity |
|
|
697
|
+
| `$\{SESSION_CONTEXT}` | Current session-scope context (project/entity/etc., substrate-defined) |
|
|
698
|
+
| `$\{TRIGGER_TYPE}` | What event fired this skill |
|
|
699
|
+
| `$\{TRIGGER_PAYLOAD}` | Event-specific data |
|
|
700
|
+
| `$\{EVENT.*}` | Event-payload fields populated by the trigger source |
|
|
701
|
+
| `$\{ERROR_CONTEXT}` | In `# OnError:` fallback skills: type + target where failure occurred |
|
|
779
702
|
|
|
780
703
|
Iterator vars from `foreach` and output bindings from runtime-intrinsic / MCP-dispatch ops also pass through ambient at compile time; the runtime substitutes them per iteration / per op completion.
|
|
781
704
|
|
|
@@ -880,14 +803,14 @@ Pipe filters apply transforms to resolved variables before substitution. Syntax:
|
|
|
880
803
|
|
|
881
804
|
| Filter | Effect | Example | Output |
|
|
882
805
|
|--------|--------|---------|--------|
|
|
883
|
-
| `url` | `encodeURIComponent(value)` |
|
|
884
|
-
| `shell` | POSIX single-quote escape with outer quotes |
|
|
885
|
-
| `json` | `JSON.stringify(value)` |
|
|
886
|
-
| `trim` | Whitespace trim |
|
|
887
|
-
| `length` | Count of items (array) or characters (string) |
|
|
888
|
-
| `contains:"X"` | Boolean: type-aware substring / element membership |
|
|
889
|
-
| `fallback:"X"` | Coalesce on missing/undefined/empty ref |
|
|
890
|
-
| `isodate` | Epoch seconds → ISO-8601 timestamp |
|
|
806
|
+
| `url` | `encodeURIComponent(value)` | `$\{location|url}` for `"Asheville, NC"` | `Asheville%2C%20NC` |
|
|
807
|
+
| `shell` | POSIX single-quote escape with outer quotes | `$\{arg|shell}` for `it's safe` | `'it'\''s safe'` |
|
|
808
|
+
| `json` | `JSON.stringify(value)` | `$\{payload|json}` for `\{k:"v"}` | `"\{\"k\":\"v\"}"` |
|
|
809
|
+
| `trim` | Whitespace trim | `$\{VERDICT|trim}` for `"urgent\n"` | `urgent` |
|
|
810
|
+
| `length` | Count of items (array) or characters (string) | `$\{ITEMS|length}` for `["a","b","c"]` | `3` |
|
|
811
|
+
| `contains:"X"` | Boolean: type-aware substring / element membership | `$\{MSG|contains:"urgent"}` for `"Yes, urgent"` | `true` |
|
|
812
|
+
| `fallback:"X"` | Coalesce on missing/undefined/empty ref | `$\{VAR.missing|fallback:"-"}` | `-` |
|
|
813
|
+
| `isodate` | Epoch seconds → ISO-8601 timestamp | `$\{EPOCH|isodate}` for `1779660000` | `2026-05-24T22:00:00.000Z` |
|
|
891
814
|
|
|
892
815
|
### `length` semantics
|
|
893
816
|
|
|
@@ -1501,11 +1424,11 @@ The substrate-routing ops (`$ connector.tool`, `$ data_read`, `$ llm`) and the a
|
|
|
1501
1424
|
|
|
1502
1425
|
## Five connector types
|
|
1503
1426
|
|
|
1504
|
-
###
|
|
1427
|
+
### DataStore
|
|
1505
1428
|
|
|
1506
|
-
Routes `$ data_read` retrieval ops. Interface: `
|
|
1429
|
+
Routes `$ data_read` retrieval ops. Interface: `DataStore.query(filters) → PortableMemory[]`.
|
|
1507
1430
|
|
|
1508
|
-
Implementations vary by deployment — a knowledge-substrate-backed store, a SQLite-backed store, a vector-DB-backed store, an in-memory test store. All conform to the `
|
|
1431
|
+
Implementations vary by deployment — a knowledge-substrate-backed store, a SQLite-backed store, a vector-DB-backed store, an in-memory test store. All conform to the `DataStore.query` contract and return `PortableMemory[]`.
|
|
1509
1432
|
|
|
1510
1433
|
### LocalModel
|
|
1511
1434
|
|
|
@@ -1621,7 +1544,7 @@ Skill records are infrastructure, not knowledge atoms — adopter impls should t
|
|
|
1621
1544
|
|
|
1622
1545
|
All connector types expose `capabilities()` for runtime discovery. Consumers:
|
|
1623
1546
|
1. `# Requires:` matching against the registered set
|
|
1624
|
-
2. Dynamic queries via `
|
|
1547
|
+
2. Dynamic queries via `listDataStores()` / `listLocalModels()` / `listMcpConnectors()` / `listAgentConnectors()` to pick a connector for the moment
|
|
1625
1548
|
3. Authoring tools that surface the registered set
|
|
1626
1549
|
|
|
1627
1550
|
## Multi-instance by design
|
|
@@ -1630,7 +1553,7 @@ Multiple instances of the same connector type are the *normal case*, not the exc
|
|
|
1630
1553
|
|
|
1631
1554
|
```
|
|
1632
1555
|
{
|
|
1633
|
-
primary:
|
|
1556
|
+
primary: DataStoreImplA,
|
|
1634
1557
|
project: SqliteProjectStore,
|
|
1635
1558
|
scratch: InMemoryStore
|
|
1636
1559
|
}
|
|
@@ -1779,7 +1702,7 @@ interface McpDispatchCtx {
|
|
|
1779
1702
|
|
|
1780
1703
|
Hard-coupling skills to specific substrates would make information-flow decisions infrastructural rather than skill-authored, defeating the point of skills as the agent's programming language. The connector layer is what lets the same skill body run against substrate A today and run against substrate B tomorrow without rewriting.
|
|
1781
1704
|
|
|
1782
|
-
## Lifecycle and status — # Status: header,
|
|
1705
|
+
## Lifecycle and status — # Status: header, three canonical states (Draft / Approved / Disabled), compile + runtime enforcement
|
|
1783
1706
|
|
|
1784
1707
|
Skillscripts carry an explicit lifecycle state via the `# Status:` header. The compiler and runtime enforce status — a Disabled skillscript cannot fire under any path, regardless of who invokes it.
|
|
1785
1708
|
|
|
@@ -2037,6 +1960,13 @@ default: fetch
|
|
|
2037
1960
|
|
|
2038
1961
|
**Recursion guard.** The runtime enforces a configurable recursion-depth limit (default 10) to prevent infinite-loop composition. Exceeding the limit raises a clean structured error attributable to the offending dispatch site, not a stack overflow.
|
|
2039
1962
|
|
|
1963
|
+
## Error surfacing in composition
|
|
1964
|
+
|
|
1965
|
+
Two non-obvious behaviors when reading a child's failures from the parent (both confirmed by the v1.0 runtime-semantics test battery):
|
|
1966
|
+
|
|
1967
|
+
- **Nested errors do NOT surface at the top level.** When a child op fails — e.g. the recursion-depth guard fires — the structured error nests inside the child's `R.errors`, which nests inside *its* parent's `R.errors`, and so on up the chain. It does **not** bubble to the caller's top-level `result.errors`. So a top-level `errors: []` does **not** mean nothing failed downstream. To detect a child failure, inspect the bound `${R.errors}` (and deeper), or rely on `(fallback: ...)` / `# OnError:` — those fire on the structured error regardless of nesting depth.
|
|
1968
|
+
- **`# Returns: R` is load-bearing for observability.** If the parent binds a child via `-> R` but does not declare `# Returns: R`, the `# Returns:` filter strips `R` from the parent's `final_vars` — and any nested child errors disappear from the parent's MCP-wire response along with it. A composition skill that wants its child's failures observable from *its own* caller must declare the binding (`# Returns: R`) explicitly. Observability of child failure is opt-in, the same way value export is.
|
|
1969
|
+
|
|
2040
1970
|
## Forward-reference resolution
|
|
2041
1971
|
|
|
2042
1972
|
Skill references (`inline(skill=...)`, `execute_skill(skill_name=...)`) are validated at compile time but allow forward references with tier-2 advisories — making it possible to author sibling skills together (chicken-and-egg).
|
|
@@ -2152,6 +2082,7 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
|
|
|
2152
2082
|
|
|
2153
2083
|
- Treat composition as a real cost. Each `execute_skill()` dispatch incurs the child's full execution time + side effects. Don't compose for trivial cases that could be inlined.
|
|
2154
2084
|
- Declare `# Returns:` when a caller needs structured access to a child's variables. Leave it off for emit-only skills whose consumers read `.outputs.text` — the default-empty filter keeps scratch from propagating.
|
|
2085
|
+
- Want a child's failures visible to your caller? Declare the child binding in `# Returns:` — nested errors are filtered out with the binding otherwise (see Error surfacing in composition), and don't trust a top-level `errors: []`.
|
|
2155
2086
|
- Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path.
|
|
2156
2087
|
- Use mechanical mode to TestFlight any multi-skill chain before shipping it as a Headless skill on a cron trigger.
|
|
2157
2088
|
- Forward references work — author sibling skills in any order, validate independently. The tier-2 warning surfaces the deferred-resolution path; runtime catches genuine misses.
|
|
@@ -2481,50 +2412,72 @@ Some features depend on others:
|
|
|
2481
2412
|
|
|
2482
2413
|
The "Not yet implemented, but planned" section at top tracks the user-facing surface; this section preserves the design-order argument for when implementation work picks up.
|
|
2483
2414
|
|
|
2484
|
-
##
|
|
2415
|
+
## Not yet implemented, but planned
|
|
2416
|
+
|
|
2417
|
+
These are features designed or anticipated but not yet implemented in the current build. Authors should not use these forms; they will not compile.
|
|
2418
|
+
|
|
2419
|
+
## Control flow
|
|
2485
2420
|
|
|
2486
|
-
|
|
2421
|
+
- **`while CONDITION:` loops** — today's iteration is `foreach IDENT in EXPR:` only. While loops are planned for ad-hoc orchestration patterns ("loop until response contains 'done'").
|
|
2422
|
+
- **Arithmetic in `$set`** — today accepts literals + `${VAR}` interpolation; no `+ - * /` operators. Planned alongside `while` for turn counters and orchestration bookkeeping.
|
|
2487
2423
|
|
|
2488
|
-
##
|
|
2424
|
+
## Pipe filter extensions
|
|
2425
|
+
|
|
2426
|
+
- **Array aggregation primitives** — `|max`, `|min`, `|sum`, `|reduce` over arrays. Today the language tops out at "shape one record" — aggregating across an array requires `foreach` accumulator ceremony. Planned as a design question: is `foreach` the deliberate ceiling for aggregation, or do we add primitives?
|
|
2427
|
+
|
|
2428
|
+
## Triggers — note
|
|
2429
|
+
|
|
2430
|
+
Trigger sources are `cron` + `event` only (see the Triggers section). There is no separate `agent-event:` / `file-watch:` / `sensor:` trigger source — anything that isn't time-based is an external adapter that POSTs to the `/event` ingress. So there's nothing pending here on the trigger axis; the entry remains only to point readers who expect those sources at the adapter-POST pattern.
|
|
2489
2431
|
|
|
2490
|
-
Within a target body, op ordering and variable binding conventions aren't fully written down. Specific questions:
|
|
2491
|
-
- Can `emit()` calls precede `$` ops in the same target? (Yes; `emit()` has no dependency on subsequent ops.)
|
|
2492
|
-
- What's the default output binding when `-> NAME` is omitted? (`${target.output}` — same as bare `target` referenced from other blocks.)
|
|
2493
|
-
- How do cross-block references work syntactically? (`${other_target.output}` or `${VAR_BOUND_THERE}`.)
|
|
2494
2432
|
|
|
2495
|
-
|
|
2433
|
+
## Tests
|
|
2496
2434
|
|
|
2497
|
-
|
|
2435
|
+
- **`# Tests:` block** with `given:` / `expect:` assertions — author-authored test cases. Will land when adopter signal demands test infrastructure.
|
|
2498
2436
|
|
|
2499
|
-
|
|
2437
|
+
## Output kinds
|
|
2500
2438
|
|
|
2501
|
-
|
|
2439
|
+
- **`# Output: file: <path>`** — file-output routing parses but no router exists today.
|
|
2440
|
+
- **`# Output: card:`** — depends on a substrate-side card render surface; not implemented.
|
|
2502
2441
|
|
|
2503
|
-
|
|
2442
|
+
## Persistent state with declared scope
|
|
2504
2443
|
|
|
2505
|
-
|
|
2444
|
+
```
|
|
2445
|
+
$set NAME = value scope=skill-local
|
|
2446
|
+
$set NAME = value scope=agent-global
|
|
2447
|
+
$set NAME = value scope=session
|
|
2448
|
+
```
|
|
2506
2449
|
|
|
2507
|
-
|
|
2450
|
+
Scopes: skill-local (persists across fires of this skill, not visible to other skills), agent-global (visible to all skills of the same agent), session (alive for the duration of the current session, cleared at session end). Backed by a configured data-records connector.
|
|
2508
2451
|
|
|
2509
|
-
## 5. Output target delivery failures
|
|
2510
2452
|
|
|
2511
|
-
|
|
2453
|
+
## Time as first-class primitives
|
|
2512
2454
|
|
|
2513
|
-
|
|
2455
|
+
Currently `$(NOW)` (wall-clock). Planned relative-time primitives:
|
|
2514
2456
|
|
|
2515
|
-
|
|
2457
|
+
```
|
|
2458
|
+
$(SECONDS_SINCE_LAST_USER_MESSAGE)
|
|
2459
|
+
$(MINUTES_SINCE_SESSION_START)
|
|
2460
|
+
$(SECONDS_SINCE_LAST_FIRE_OF.<skill-name>)
|
|
2461
|
+
```
|
|
2516
2462
|
|
|
2517
|
-
|
|
2463
|
+
Most "right time" reasoning is relative, not wall-clock.
|
|
2518
2464
|
|
|
2519
|
-
|
|
2465
|
+
## Other planned
|
|
2520
2466
|
|
|
2521
|
-
|
|
2467
|
+
- **Absence-as-trigger** — `# Triggers: idle: 5m` fire-on-quiet primitive (would be a new trigger source beyond `cron`|`event`, or an adapter that POSTs on a timer)
|
|
2468
|
+
- **Time-windowed aggregation** — filter-like primitives across firings (e.g., "user has shown frustration in 3 of 5 recent turns")
|
|
2469
|
+
- **Debounce / rate-limit / coalesce** — declarative queueing policy headers
|
|
2470
|
+
- **Suppression as valid output** — explicit "fire-and-suppress" (different from `# Output: none`)
|
|
2471
|
+
- **Cross-skill pub-sub** — `# Publishes: signal.X` / `# Subscribes: signal.Y` decoupling
|
|
2472
|
+
- **Confidence/threshold gating** — `# RequiresConfidence: classifier >= 0.8` / `# RequiresThreshold:`
|
|
2473
|
+
- **Invocation-control axis** — `# Invocable-By: user | agent | trigger` (sensitive ops shouldn't leak across invocation boundaries)
|
|
2474
|
+
- **Introspection primitives** — `$(PROMPT_CONTEXT.size)`, `$(SKILLS_FIRED_RECENTLY.last-1h)`, `$(SELF.confidence-trend)`
|
|
2522
2475
|
|
|
2523
|
-
|
|
2476
|
+
## When the language extends, this section shrinks
|
|
2524
2477
|
|
|
2525
|
-
|
|
2478
|
+
When any of these primitives ship, the relevant grammar moves into its canonical section (Ops reference, Variables, Triggers, etc.) and the entry here is removed. This section stays alive as a continuous staging area for the next horizon of unshipped work.
|
|
2526
2479
|
|
|
2527
2480
|
---
|
|
2528
2481
|
|
|
2529
|
-
*Rendered from `skillscript/skillscript-language-reference` — 2026-06-15
|
|
2482
|
+
*Rendered from `skillscript/skillscript-language-reference` — 2026-06-16 15:46 EDT*
|
|
2530
2483
|
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: SqliteSkillStore
|
|
3
|
+
description: "Worked example of SkillStore against a SQLite database — schema, semantics, forking checklist."
|
|
4
|
+
mode: wide
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
Worked example of `SkillStore` against a SQLite database. Copy this directory into your codebase, customize per your storage needs, register with skillscript-runtime's `Registry`. This README is written for the agent implementing your adopter's connector — including the human reviewing the PR.
|
|
4
8
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Skill: classify-support-ticket
|
|
2
|
-
# Status:
|
|
2
|
+
# Status: Draft
|
|
3
3
|
# Autonomous: true
|
|
4
4
|
# Description: Read an incoming support ticket and route it: severity-1 tickets get paged to ops-channel, billing tickets get tagged for finance review, everything else gets a draft reply queued for human review
|
|
5
5
|
# Vars: TICKET_TEXT, TICKET_ID
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Skill: data-store-roundtrip
|
|
2
|
-
# Status:
|
|
2
|
+
# Status: Draft
|
|
3
3
|
# Autonomous: true
|
|
4
4
|
# Description: Round-trips the DataStore — writes a record tagged with a per-run marker in the content, reads it back via FTS query against the same marker. The exact item count depends on substrate FTS matching strictness (strict FTS substrates return 1; substrates with looser token-match semantics may return prior runs' records that share token shapes). If this skill executes and the output line shows N ≥ 1 items returned, your DataStore substrate is wired correctly. For adopters needing deterministic counts (e.g. authoring round-trip tests), the `domain_tags` filter is the portable strict-match read path — see the adopter-playbook §"Notable things..." for the pattern.
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Skill: feedback-sentiment-scan
|
|
2
|
-
# Status:
|
|
2
|
+
# Status: Draft
|
|
3
3
|
# Description: Each night, scan the previous 24h of customer feedback records, classify sentiment via the local model, and surface entries that read 'frustrated' or 'blocking' so the team sees them at start-of-day.
|
|
4
4
|
# Triggers: cron: 0 3 * * *
|
|
5
5
|
# Vars: SCAN_LIMIT=50
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Skill: hello-world
|
|
2
|
-
# Status:
|
|
2
|
+
# Status: Draft
|
|
3
3
|
# Description: The canonical first-run example. No substrate dependencies, no compute block — pure declarative output. If this fails to execute, your install or runtime is broken; investigate before troubleshooting deeper layers. Demonstrates the template-only shape: body text IS the skill; `# Vars:` declared inputs feed the template with `--input` override.
|
|
4
4
|
# Vars: WHO=world
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Skill: morning-brief
|
|
2
|
-
# Status:
|
|
2
|
+
# Status: Draft
|
|
3
3
|
# Description: Compose a daily morning brief from calendar, mailbox, and overnight notes when the cron trigger fires at 7am. Delivers via the agent: lifecycle hook to the receiving agent, who decides whether to surface to Slack / Discord / etc. Requires a `calendar` connector configured in connectors.json (the dotted `$ calendar.*` form). `model=qwen` is a representative LocalModel alias — adopters register one under whatever name fits (the bundled bootstrap registers `default`).
|
|
4
4
|
# Vars: AGENT, BRIEF_HORIZON_HOURS=24
|
|
5
5
|
# Triggers: cron: 0 7 * * *
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Skill: queue-length-monitor
|
|
2
|
-
# Status:
|
|
2
|
+
# Status: Draft
|
|
3
3
|
# Description: Count pending items in a queue and alert when the count exceeds threshold. Requires `cat` on the operator's shell allowlist (default-deny: a non-allowlisted binary is refused).
|
|
4
4
|
# Vars: QUEUE_PATH=/var/queue/pending.json, THRESHOLD=10
|
|
5
5
|
# Triggers: cron: */5 * * * *
|