skillscript-runtime 0.15.3 → 0.15.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.
@@ -162,6 +162,11 @@ Create `skillscript.config.json` in your `$SKILLSCRIPT_HOME`:
162
162
 
163
163
  **For custom substrates**: write your own bootstrap. See `examples/custom-bootstrap.example.ts` and `examples/onboarding-scaffold/bootstrap.ts` for complete worked walkthroughs.
164
164
 
165
+ Two security knobs that adopters wiring real substrates should know about:
166
+
167
+ - **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. See `docs/configuration.md` §"Named MCP connector instances" for the JSON shape.
168
+ - **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.
169
+
165
170
  If you have a custom JSON-instantiable `McpConnector` class, register it with `registerConnectorClass` before loading config:
166
171
 
167
172
  ```typescript
@@ -241,8 +246,12 @@ See `examples/custom-bootstrap.example.ts` for a worked walkthrough.
241
246
  - **`SqliteDataStore` is a deliberately minimal reference impl.** It satisfies the contract (`query` / `write` / `get` / `staticCapabilities` / `manifest`) with FTS-style tag/text retrieval. It does NOT support semantic retrieval, pinning, decay scoring, or thread-status filtering (the relevant `supports_*` flags are all false). Deployments that need richer query semantics fork `examples/connectors/DataStoreTemplate/` and wire their backing substrate.
242
247
  - **SkillStore and DataStore have different lifecycle models — by design.** SkillStore is mutable / versioned / named CRUD (Draft→Approved→Disabled→Delete with audit trail). DataStore is append-only with query/get (no per-record lifecycle in the contract). If you back both onto one substrate, you're serving both lifecycle models at once. Substrates that conflate "data record expiry" with "skill expiry" silently break authored code; the contract doesn't enforce this, you handle it impl-side.
243
248
  - **Durability is implementer's responsibility.** The typed contracts assume durable storage. Neither interface declares "writes live forever" — but the runtime + lint + dashboard all behave as if writes persist indefinitely. If your substrate has GC / TTL / decay scoring, build adopter-side guards (pin-rules, retention policies, periodic re-pin sweeps) or pick a substrate posture that satisfies "durable forever." Silent staleness is the failure mode the contract won't catch.
244
- - **Mutation ops require runtime-enforced authorization (v0.14.1).** `$ data_write` / `file_write` / `$ <mutating-name-tool>` (write/update/delete/etc.) fire `UnconfirmedMutationError` at the runtime boundary unless the skill carries `# Autonomous: true` (cron/agent-fired) OR a preceding `??` / `ask(...)` confirms in the same target OR the op carries `approved="reason"` per-op kwarg. This fires regardless of how the skill was invoked — `execute_skill({skill_name})` AND `execute_skill({source})` honor the gate identically; lint stays advisory. Adopters running unattended skills programmatically should set `# Autonomous: true` at the header.
245
- - **Filter scope was advisory in v0.14.0; v0.14.1 makes it enforceable.** Pre-v0.14.1, `query(filters)` accepted arbitrary fields and substrates silently dropped what they didn't support. Pass-through is fine for non-scope-sensitive filters but is a leak for security-relevant ones. v0.14.1 ships strict default enforcement at the `DataStoreMcpConnector` bridge: filter keys outside the substrate's declared `supported_filters` manifest throw `UnsupportedFilterError`. Adopters who want pre-v0.14.1 permissive behavior opt out per-call via `permissive_filters: true`. Substrate implementors: declare your honored filters in `manifest().supported_filters` so the bridge validates against your truth, not a guess.
249
+ - **Mutation ops require runtime-enforced authorization.** `$ data_write` / `file_write` / `$ <mutating-name-tool>` (write/update/delete/etc.) fire `UnconfirmedMutationError` at the runtime boundary unless the skill carries `# Autonomous: true` (cron/agent-fired) OR a preceding `??` / `ask(...)` confirms in the same target OR the op carries `approved="reason"` per-op kwarg. This fires regardless of how the skill was invoked — `execute_skill({name})` AND `execute_skill({source})` honor the gate identically; lint stays advisory. Adopters running unattended skills programmatically should set `# Autonomous: true` at the header.
250
+ - **In-skill writes have asymmetric trust models.** `$ skill_write` lands its child as `# Status: Draft` regardless of body declaration the bridge forces it. Authoring an executable artifact has unbounded blast radius (the child fires arbitrarily many times in arbitrary contexts); the Draft default keeps autonomously-written skills out of the immediate execution loop. `$ data_write` writes verbatim one bad data row is bounded blast radius. SkillStore impls receive the body already Draft-stamped; DataStore impls receive entries as authored.
251
+ - **Your `DataStore.write()` is never called if the mutation gate rejects the skill.** The runtime gates `$ data_write` (and other mutation ops) upstream of the bridge — substrates only see authorized writes. If your own probes hit `UnconfirmedMutationError`, that's a skill-body issue (missing `# Autonomous: true` / `??` / `approved=`), not a substrate issue.
252
+ - **Filter scope is enforced at the bridge.** `DataStoreMcpConnector` rejects every filter key outside the substrate's declared `manifest().supported_filters` set, throwing `UnsupportedFilterError`. This prevents silent scope leaks where unsupported filters get dropped without the caller knowing. Per-call opt-out: `permissive_filters: true` acknowledges "unknown keys are advisory; substrate may ignore them." Substrate implementors: declare every filter your `query()` actually honors so the bridge validates against your truth, not a guess.
253
+ - **FTS matching strictness varies by substrate.** The `DataStore.query()` contract names the modes (`fts` / `semantic` / `rerank`) but doesn't pin down matching semantics within each mode — token-OR, phrase-tokens, fuzzy, exact, FTS5-syntax-passthrough, etc. are all conformant. The bundled `data-store-roundtrip` demo asserts `N ≥ 1` (a successful round-trip) rather than a specific count, which works across any FTS-supporting substrate. For adopters who need deterministic exact-count reads (round-trip tests, idempotency checks, exact-record-matched fetches), the portable strict-match path is `domain_tags=[...]` filtering — the bridge enforces tag-key against `supported_filters` and substrates declaring `supports_tag_filter: true` honor exact-tag any-of-match per the contract. Use FTS for relevance ranking against open content; use tag filters when you need to be sure you got the specific record you wrote.
254
+ - **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.
246
255
  - **4 of 6 trigger sources parse but don't fire.** `cron` and `session: start` work; `event`, `agent-event`, `file-watch`, `sensor` are parser-only stubs awaiting the event-bus surface.
247
256
  - **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.
248
257
  - **Authorization is hash-token approval.** Skills must carry `# Status: Approved vN:<token>` where the token re-computes from the body minus its `# Status:` line. Bundled `v1:` is CRC32 — discipline-barrier strength, suited to single-operator deployments. Adversarial threat models swap a stronger function:
@@ -1,12 +1,12 @@
1
1
  # Skill: data-store-roundtrip
2
- # Status: Approved v1:b6e1bfb1
2
+ # Status: Approved v1:fb106073
3
3
  # Autonomous: true
4
- # Description: Round-trips the DataStore — writes a record tagged with a per-run marker, reads it back via full-text search against the same marker. The marker makes the read deterministic (count == 1) regardless of prior runs accumulated in the store. If this skill executes and emits "read back 1 items", your DataStore substrate is wired correctly.
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 emit 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
 
6
6
  run:
7
7
  $set MARKER = "probe-${NOW}"
8
8
  $ data_write content="${MARKER} adopter dogfood probe record" tags=["phase1","probe"] -> W
9
9
  $ data_read mode="fts" query="${MARKER}" limit=5 -> R
10
- emit(text="wrote record '${W.id}'; read back ${R.items|length} items")
10
+ emit(text="wrote record '${W.id}'; read back ${R.items|length} item(s)")
11
11
 
12
12
  default: run
@@ -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-01T18:34:24.038Z",
5
+ "compiled_at": "2026-06-01T20:23:35.853Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
@@ -1,10 +1,10 @@
1
1
  # Skill: skill-store-roundtrip
2
- # Status: Approved v1:a3d46cf0
2
+ # Status: Approved v1:04b49f15
3
3
  # Autonomous: true
4
4
  # Description: Round-trips the SkillStore — writes a child skill, reads it back. Demonstrates the Lisp-shape primitive (skills can write skills). NOTE: under v0.15.0, in-skill `$ skill_write` lands the child as `# Status: Draft` regardless of what the body declares — to run the generated child, an authorized agent (human via dashboard, or MCP-direct) reviews + promotes via the outside-MCP `skill_status` tool. The Draft-default gate keeps autonomously-written skills out of the immediate execution loop.
5
5
 
6
6
  run:
7
- $ skill_write name="hello-child" source="# Skill: hello-child\n# Status: Approved\nrun:\n emit(text=\"hello from a programmatically-authored skill\")\ndefault: run\n" -> W
7
+ $ skill_write name="hello-child" source="# Skill: hello-child\n# Status: Approved\nrun:\n emit(text=\"hello from a programmatically-authored skill\")\ndefault: run\n" overwrite=true -> W
8
8
  $ skill_read name="hello-child" -> R
9
9
  emit(text="wrote skill '${W.name}' as Status: ${W.status}; read back ${R.source|length} bytes")
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscript-runtime",
3
- "version": "0.15.3",
3
+ "version": "0.15.5",
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>",
@@ -1,12 +1,12 @@
1
1
  # Skill: data-store-roundtrip
2
- # Status: Approved v1:b6e1bfb1
2
+ # Status: Approved v1:fb106073
3
3
  # Autonomous: true
4
- # Description: Round-trips the DataStore — writes a record tagged with a per-run marker, reads it back via full-text search against the same marker. The marker makes the read deterministic (count == 1) regardless of prior runs accumulated in the store. If this skill executes and emits "read back 1 items", your DataStore substrate is wired correctly.
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 emit 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
 
6
6
  run:
7
7
  $set MARKER = "probe-${NOW}"
8
8
  $ data_write content="${MARKER} adopter dogfood probe record" tags=["phase1","probe"] -> W
9
9
  $ data_read mode="fts" query="${MARKER}" limit=5 -> R
10
- emit(text="wrote record '${W.id}'; read back ${R.items|length} items")
10
+ emit(text="wrote record '${W.id}'; read back ${R.items|length} item(s)")
11
11
 
12
12
  default: run
@@ -1,10 +1,10 @@
1
1
  # Skill: skill-store-roundtrip
2
- # Status: Approved v1:a3d46cf0
2
+ # Status: Approved v1:04b49f15
3
3
  # Autonomous: true
4
4
  # Description: Round-trips the SkillStore — writes a child skill, reads it back. Demonstrates the Lisp-shape primitive (skills can write skills). NOTE: under v0.15.0, in-skill `$ skill_write` lands the child as `# Status: Draft` regardless of what the body declares — to run the generated child, an authorized agent (human via dashboard, or MCP-direct) reviews + promotes via the outside-MCP `skill_status` tool. The Draft-default gate keeps autonomously-written skills out of the immediate execution loop.
5
5
 
6
6
  run:
7
- $ skill_write name="hello-child" source="# Skill: hello-child\n# Status: Approved\nrun:\n emit(text=\"hello from a programmatically-authored skill\")\ndefault: run\n" -> W
7
+ $ skill_write name="hello-child" source="# Skill: hello-child\n# Status: Approved\nrun:\n emit(text=\"hello from a programmatically-authored skill\")\ndefault: run\n" overwrite=true -> W
8
8
  $ skill_read name="hello-child" -> R
9
9
  emit(text="wrote skill '${W.name}' as Status: ${W.status}; read back ${R.source|length} bytes")
10
10