skillscript-runtime 0.20.1 → 0.21.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.
@@ -15,6 +15,8 @@ const state = {
15
15
  blockedShellAttempts: null,
16
16
  // v1.0 Gate #7 — { enabled, public_key_present } or null (pre-Gate-#7 server).
17
17
  securedApproval: null,
18
+ // v0.20.2 — true when the dashboard can sign in-browser (passcode unlock wired).
19
+ dashboardSigning: false,
18
20
  lastUpdate: null,
19
21
  };
20
22
 
@@ -45,11 +47,14 @@ async function refresh() {
45
47
  const ts = new Date();
46
48
  document.getElementById("poll-status").textContent = `polling…`;
47
49
  try {
48
- const [catalog, triggers, metrics, capabilities, blocked] = await Promise.all([
49
- // v0.9.8skill_list returns SkillCatalog (pre-grouped by audience).
50
- // Dashboard wants the full picture (all categories); flatten the
51
- // groups into a single array for the existing rendering code.
52
- callTool("skill_list", { filter: { audience: "all" } }),
50
+ const [draftCat, apprCat, disabledCat, triggers, metrics, capabilities, blocked] = await Promise.all([
51
+ // v0.20.2fetch ALL statuses so the Skills view shows Draft + Approved +
52
+ // Disabled (skill_list defaults to Approved-only, which hid Disabled skills
53
+ // entirely you couldn't find one to re-enable). Three calls + merge since
54
+ // skill_list has no "all-statuses" filter.
55
+ callTool("skill_list", { filter: { audience: "all", status: "Draft" } }),
56
+ callTool("skill_list", { filter: { audience: "all", status: "Approved" } }),
57
+ callTool("skill_list", { filter: { audience: "all", status: "Disabled" } }),
53
58
  callTool("list_triggers", {}),
54
59
  callTool("health_metrics", {}),
55
60
  callTool("runtime_capabilities", { include: ["mcpConnectors", "mcpConnectorClasses", "localModels", "dataStores", "skillStores", "agentConnectors", "securedApproval", "runtimeVersion"] }),
@@ -58,20 +63,23 @@ async function refresh() {
58
63
  // this tool; catch returns null and the Security view degrades.
59
64
  callTool("blocked_shell_attempts", { limit: 100 }).catch(() => null),
60
65
  ]);
61
- state.skills = [
62
- ...(catalog.receives ?? []),
63
- ...(catalog.skills ?? []),
64
- ...(catalog.headless ?? []),
65
- ];
66
+ const flatCatalog = (c) => [...(c?.receives ?? []), ...(c?.skills ?? []), ...(c?.headless ?? [])];
67
+ // Approved first, then Draft, then Disabled — grouped by lifecycle.
68
+ state.skills = [...flatCatalog(apprCat), ...flatCatalog(draftCat), ...flatCatalog(disabledCat)];
66
69
  state.triggers = triggers;
67
70
  state.metrics = metrics;
68
71
  state.capabilities = capabilities;
69
72
  // v0.9.0+ servers may omit securedApproval (pre-Gate-#7) → null = "unsecured".
70
73
  state.securedApproval = capabilities?.securedApproval ?? null;
74
+ // v0.20.2 — can the dashboard sign in-browser? (passcode unlock wired)
75
+ state.dashboardSigning = await fetch("/signing-status").then((r) => r.json()).then((j) => j?.enabled === true).catch(() => false);
71
76
  state.blockedShellAttempts = blocked;
72
77
  state.lastUpdate = ts;
73
78
  renderSecuredBanner();
74
- document.getElementById("poll-status").textContent = `last updated ${ts.toLocaleTimeString()}`;
79
+ // v0.21.0 surface the running runtime version next to the poll timestamp.
80
+ const ver = state.capabilities?.runtimeVersion;
81
+ document.getElementById("poll-status").textContent =
82
+ `last updated ${ts.toLocaleTimeString()}${ver ? ` · skillscript v${ver}` : ""}`;
75
83
  renderCurrentView();
76
84
  } catch (err) {
77
85
  document.getElementById("poll-status").textContent = `poll failed: ${err.message}`;
@@ -119,11 +127,12 @@ function renderSecuredBanner() {
119
127
  </div>`;
120
128
  return;
121
129
  }
130
+ const approvalLine = state.dashboardSigning
131
+ ? `Approve in-browser after a one-time passcode unlock (or <code>skillfile approve &lt;name&gt;</code> at a terminal).`
132
+ : `Approval requires the operator's key: <code>skillfile approve &lt;name&gt;</code> at the terminal — the dashboard reviews, it does not sign.`;
122
133
  el.innerHTML = `<div class="banner banner-secured">
123
134
  <strong>🔒 Secured mode</strong> — unapproved skills cannot execute any
124
- effectful op. Approval requires the operator's key:
125
- <code>skillfile approve &lt;name&gt;</code> at the terminal. The dashboard
126
- reviews; it does not sign.
135
+ effectful op. ${approvalLine}
127
136
  </div>`;
128
137
  }
129
138
 
@@ -157,10 +166,13 @@ async function renderApprovals() {
157
166
  return `<h2>Approvals</h2><section><div class="empty">Failed to load the approval queue: ${esc(err.message)}</div></section>`;
158
167
  }
159
168
  const staleCount = pending.filter((s) => s._why === "stale").length;
169
+ const signLine = state.dashboardSigning
170
+ ? `Review the source, then click <strong>Approve</strong> (a one-time passcode unlocks signing for this session).`
171
+ : `Review the source, then sign at a terminal that can read your approval key.`;
160
172
  const intro = secured
161
173
  ? `<p>Secured mode is <strong>ON</strong>. Each skill below is inert — no
162
- effectful op will run until it is approved. ${staleCount > 0 ? `${staleCount} carry a stale/legacy approval (e.g. a pre-secured v1 stamp) and need re-signing — the fastest path is <code>skillfile reapprove --apply</code> at a terminal.` : ""}
163
- Review the source, then sign at a terminal that can read your approval key.</p>`
174
+ effectful op will run until it is approved. ${staleCount > 0 ? `${staleCount} carry a stale/legacy approval (e.g. a pre-secured v1 stamp) and need re-signing${state.dashboardSigning ? "" : ` — the fastest path is <code>skillfile reapprove --apply</code> at a terminal`}.` : ""}
175
+ ${signLine}</p>`
164
176
  : `<p>Secured mode is <strong>OFF</strong> — Draft skills can be approved
165
177
  in-page from their detail view (the runtime self-stamps). Turn on secured
166
178
  mode (<code>SKILLSCRIPT_SECURED_MODE=true</code>) to require key-signed
@@ -188,7 +200,9 @@ async function renderApprovals() {
188
200
  <td>${sigBadges}</td>
189
201
  <td>
190
202
  <a href="#skill/${encodeURIComponent(s.name)}">Review →</a>
191
- ${secured ? `<div class="approve-cmd"><code>${esc(cmd)}</code><button class="copy-btn" onclick="copyText('${esc(cmd).replace(/'/g, "\\'")}', this)">copy</button></div>` : ""}
203
+ ${!secured ? "" : (state.dashboardSigning
204
+ ? `<button class="primary" onclick="approveInBrowser('${esc(s.name).replace(/'/g, "\\'")}')">Approve</button>`
205
+ : `<div class="approve-cmd"><code>${esc(cmd)}</code><button class="copy-btn" onclick="copyText('${esc(cmd).replace(/'/g, "\\'")}', this)">copy</button></div>`)}
192
206
  </td>
193
207
  </tr>`;
194
208
  }).join("");
@@ -217,6 +231,38 @@ function approvalSignalBadges(sig) {
217
231
  return b.length ? b.join(" ") : `<span class="badge ok">no signals</span>`;
218
232
  }
219
233
 
234
+ // v0.20.2 — in-browser approval. Click Approve → POST /approve. If the session
235
+ // isn't unlocked, prompt for the passcode once (POST /unlock), then retry. The
236
+ // unlock is session-scoped server-side, so subsequent approvals don't re-prompt.
237
+ async function postJson(path, body) {
238
+ return fetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
239
+ }
240
+
241
+ window.approveInBrowser = async function (name) {
242
+ try {
243
+ let res = await postJson("/approve", { name });
244
+ if (res.status === 401) {
245
+ const body = await res.json().catch(() => ({}));
246
+ if (body.needs_passcode) {
247
+ const pass = window.prompt(`Enter the approval passcode to sign '${name}'.\n(Unlocks signing for ~15 min — you can approve more without re-entering.)`);
248
+ if (!pass) return;
249
+ const unlock = await postJson("/unlock", { passcode: pass });
250
+ if (unlock.status !== 200) { alert("Incorrect passcode."); return; }
251
+ res = await postJson("/approve", { name });
252
+ }
253
+ }
254
+ const out = await res.json().catch(() => ({}));
255
+ if (res.status === 200 && out.approved) {
256
+ await refresh();
257
+ renderCurrentView();
258
+ } else {
259
+ alert(`Approve failed: ${out.error || ("HTTP " + res.status)}`);
260
+ }
261
+ } catch (err) {
262
+ alert(`Approve failed: ${err.message}`);
263
+ }
264
+ };
265
+
220
266
  window.copyText = function (text, btn) {
221
267
  const done = () => { if (btn) { const o = btn.textContent; btn.textContent = "copied"; setTimeout(() => { btn.textContent = o; }, 1200); } };
222
268
  if (navigator.clipboard?.writeText) {
@@ -307,7 +353,7 @@ function renderSkills() {
307
353
  ${state.skills.map((s) => `
308
354
  <tr onclick="window.location.hash='#skill/${encodeURIComponent(s.name)}'">
309
355
  <td><strong>${esc(s.name)}</strong></td>
310
- <td><span class="badge ${esc(s.status)}">${esc(s.status)}</span></td>
356
+ <td>${skillStatusBadge(s)}</td>
311
357
  <td>${esc(s.description ?? "—")}</td>
312
358
  <td><code>${esc(s.version?.slice(0, 8) ?? "—")}</code></td>
313
359
  </tr>
@@ -318,16 +364,28 @@ function renderSkills() {
318
364
  `;
319
365
  }
320
366
 
367
+ // v0.21.0 — gate-aware status badge (adopter finding 576632ca). A legacy skill
368
+ // stored `Approved` but failing the secured gate (e.g. a v1 stamp) WON'T run —
369
+ // badging it a plain green "Approved" while the Approvals tab lists it as
370
+ // "needs approval" reads as a contradiction. Show "re-approval needed" here too
371
+ // so both views agree on the truth (what the gate will actually do).
372
+ function skillStatusBadge(s) {
373
+ if (s.status === "Approved" && s.gate_ok === false) {
374
+ return `<span class="badge error" title="Approved status, but the body lacks a valid signature — won't run until re-approved">re-approval needed</span>`;
375
+ }
376
+ return `<span class="badge ${esc(s.status)}">${esc(s.status)}</span>`;
377
+ }
378
+
321
379
  async function renderSkillDetail(name) {
322
380
  try {
323
- // v0.13.3 — source moved out of skill_metadata into dedicated skill_read.
381
+ // v0.13.3 — source moved out of skill_preflight into dedicated skill_read.
324
382
  // Call both in parallel; skill_read may fail if the skill name doesn't
325
383
  // resolve (load() throws), so guard it to keep the detail view rendering.
326
384
  const [meta, readResult] = await Promise.all([
327
- callTool("skill_metadata", { name }),
385
+ callTool("skill_preflight", { name }),
328
386
  callTool("skill_read", { name }).catch(() => null),
329
387
  ]);
330
- const { metadata, versions, recent_fires, approval } = meta;
388
+ const { metadata, versions, recent_fires, approval, contract } = meta;
331
389
  const source = readResult?.source ?? null;
332
390
  const metrics = state.metrics?.perSkill?.[name];
333
391
  const triggersForSkill = state.triggers.filter((t) => t.skillName === name);
@@ -354,6 +412,8 @@ async function renderSkillDetail(name) {
354
412
  ${renderStatusActions(name, metadata, approval)}
355
413
  </section>
356
414
 
415
+ ${renderEffectfulFootprintPanel(contract?.effectful_footprint)}
416
+
357
417
  ${source ? renderSecuritySignalsPanel(source) : ""}
358
418
 
359
419
  <section>
@@ -460,19 +520,32 @@ function renderStatusActions(name, metadata, approval) {
460
520
  .join("");
461
521
 
462
522
  // The Approved action. Shown when not-yet-Approved OR stale (needs re-stamp).
523
+ // v0.20.2 — a Disabled skill is "re-enabled" (the user's mental model); in
524
+ // secured mode re-enabling = re-signing, since disabling stripped the token.
525
+ const isDisabled = status === "Disabled";
463
526
  const needsApprove = status !== "Approved" || staleApproved;
527
+ const headText = isDisabled ? "Re-enable this skill" : (staleApproved ? "Re-approve (body changed since signing)" : "Approve this skill");
528
+ const btnText = isDisabled ? "Re-enable" : "Approve";
464
529
  let approveBlock = "";
465
530
  if (needsApprove) {
466
- if (secured) {
531
+ if (secured && state.dashboardSigning) {
532
+ // v0.20.2 — in-browser signing wired: click to approve (passcode-unlocked).
533
+ approveBlock = `
534
+ <div class="approve-panel">
535
+ <div class="approve-panel-head">${headText}</div>
536
+ <p>Signs with the operator's key after a one-time passcode unlock (review the source below first).</p>
537
+ <button class="primary" onclick="approveInBrowser('${esc(name).replace(/'/g, "\\'")}')">${btnText}</button>
538
+ </div>`;
539
+ } else if (secured) {
467
540
  const cmd = approveCommand(name);
468
541
  approveBlock = `
469
542
  <div class="approve-panel">
470
- <div class="approve-panel-head">${staleApproved ? "Re-approve (body changed since signing)" : "Approve this skill"}</div>
471
- <p>Secured mode: approval is signed with the operator's key at a terminal — not from this dashboard. Review the source below, then run:</p>
543
+ <div class="approve-panel-head">${headText}</div>
544
+ <p>Secured mode: ${isDisabled ? "re-enabling re-signs the skill" : "approval is signed"} with the operator's key at a terminal — not from this dashboard. Review the source below, then run:</p>
472
545
  <div class="approve-cmd"><code>${esc(cmd)}</code><button class="copy-btn" onclick="copyText('${esc(cmd).replace(/'/g, "\\'")}', this)">copy</button></div>
473
546
  </div>`;
474
547
  } else {
475
- approveBlock = `<button onclick="updateStatus('${esc(name)}','Approved')">${staleApproved ? "Re-approve (refresh token)" : "Transition to Approved"}</button>`;
548
+ approveBlock = `<button onclick="updateStatus('${esc(name)}','Approved')">${isDisabled ? "Re-enable" : (staleApproved ? "Re-approve (refresh token)" : "Transition to Approved")}</button>`;
476
549
  }
477
550
  }
478
551
 
@@ -570,6 +643,49 @@ function renderHighlightedSkillBody(source) {
570
643
  // showing aggregated counts so a reviewer knows WHAT to look for before
571
644
  // scanning the body. Pairs with renderHighlightedSkillBody() (the
572
645
  // WHERE).
646
+ // v0.21.0 — the authoritative "what does it touch" least-privilege checklist
647
+ // for the human approver. Unlike renderSecuritySignalsPanel (regex over the
648
+ // source), this is the AST-derived effectful_footprint from skill_preflight —
649
+ // the SAME op enumeration the capability gate authorizes — so it's the truth of
650
+ // what the skill can do when signed, not a textual approximation. Surfaced right
651
+ // at the approve action so the operator sees the surface they're signing off.
652
+ function renderEffectfulFootprintPanel(fp) {
653
+ if (!fp) return ""; // source not loadable / parse failed → no contract to show
654
+ const items = [];
655
+ if (fp.connectors.length > 0) {
656
+ items.push(`<li class="sig-medium-text">Dispatches to ${fp.connectors.length} MCP connector${fp.connectors.length === 1 ? "" : "s"}: ${fp.connectors.map((c) => `<code>${esc(c)}</code>`).join(", ")}</li>`);
657
+ }
658
+ if (fp.builtins.length > 0) {
659
+ items.push(`<li class="sig-info-text">Uses ${fp.builtins.length} built-in op${fp.builtins.length === 1 ? "" : "s"}: ${fp.builtins.map((b) => `<code>${esc(b)}</code>`).join(", ")}</li>`);
660
+ }
661
+ if (fp.file_writes > 0) {
662
+ items.push(`<li class="sig-high-text">${fp.file_writes} <code>file_write</code> op${fp.file_writes === 1 ? "" : "s"} <small>(writes to the filesystem allowlist)</small></li>`);
663
+ }
664
+ if (fp.file_reads > 0) {
665
+ items.push(`<li class="sig-medium-text">${fp.file_reads} <code>file_read</code> op${fp.file_reads === 1 ? "" : "s"}</li>`);
666
+ }
667
+ if (fp.unsafe_shell > 0) {
668
+ items.push(`<li class="sig-high-text">${fp.unsafe_shell} unsafe (full-bash) shell op${fp.unsafe_shell === 1 ? "" : "s"}</li>`);
669
+ }
670
+ const safeShellBins = fp.shell_binaries.filter((b) => b !== "bash" || fp.unsafe_shell === 0);
671
+ if (safeShellBins.length > 0) {
672
+ items.push(`<li class="sig-medium-text">Runs shell ${safeShellBins.length === 1 ? "binary" : "binaries"}: ${safeShellBins.map((b) => `<code>${esc(b)}</code>`).join(", ")} <small>(allowlist-gated)</small></li>`);
673
+ }
674
+ if (fp.notifies > 0) {
675
+ items.push(`<li class="sig-medium-text">${fp.notifies} <code>notify</code> agent-wake op${fp.notifies === 1 ? "" : "s"}</li>`);
676
+ }
677
+ const body = items.length === 0
678
+ ? `<p class="empty">Pure — no connector, shell, file, or notify ops. Nothing effectful to authorize.</p>`
679
+ : `<ul class="security-signals">${items.join("")}</ul>
680
+ <p><small>This is the AST-derived footprint the capability gate authorizes — the surface this skill can touch once approved. Confirm every line is least-privilege before signing.</small></p>`;
681
+ return `
682
+ <section>
683
+ <h2>What this skill touches</h2>
684
+ ${body}
685
+ </section>
686
+ `;
687
+ }
688
+
573
689
  function renderSecuritySignalsPanel(source) {
574
690
  const sig = collectSecuritySignals(source);
575
691
  const items = [];
@@ -901,7 +1017,7 @@ async function loadComposeContract(detailsEl) {
901
1017
  panel.dataset.loaded = "true";
902
1018
  const [kind, name] = detailsEl.dataset.composeRef.split(":");
903
1019
  try {
904
- const meta = await callTool("skill_metadata", { name });
1020
+ const meta = await callTool("skill_preflight", { name });
905
1021
  const m = meta.metadata;
906
1022
  // For inline (data-only), also fetch the body since it bakes at
907
1023
  // compile time — reviewer sees what literally lands in the
@@ -14,23 +14,24 @@ import type { Registry } from "./connectors/registry.js";
14
14
  * protocol conforms to MCP — real MCP clients (Claude Desktop, Cursor,
15
15
  * future tools) can consume the server unchanged.
16
16
  *
17
- * Surface: tools wrapping existing T6 primitives.
17
+ * Surface: tools wrapping existing T6 primitives, ordered by the cold-author
18
+ * workflow loop the descriptions teach (learn → discover → draft → commit →
19
+ * approve → run → automate → observe).
18
20
  *
19
- * skill_list({filter?}) SkillCatalog (v0.9.8 pre-grouped by audience)
20
- * skill_metadata({name}) metadata + version history + recent_fires + approval (no source — see skill_read)
21
- * skill_read({name, version?}) {name, version, status, source} (v0.13.3 symmetric peer to skill_write)
22
- * data_read({id, store?}) PortableData | null (v0.13.8 direct lookup; no data_write MCP by design)
23
- * skill_status({name, new_state})→ SkillStatus update (write)
24
- * list_triggers({filter?}) TriggerRegistration[]
25
- * register_trigger({...}) TriggerRegistration (write)
26
- * unregister_trigger({trigger_id})→ boolean (write)
27
- * health_metrics({filter?}) HealthMetrics
28
- * runtime_capabilities({include?})→ wired connectors + shell-exec mode (v0.2.1)
29
- * lint_skill({source?|name}) diagnostics across tiers (v0.2.3)
30
- * compile_skill({source?|name, inputs?})→ rendered artifact + errors (v0.2.3)
31
- * skill_write({name, source, overwrite?})→ commit to SkillStore (write, v0.2.3)
32
- * execute_skill({skill_name, inputs?, mechanical?})→ run + return result (write, v0.2.8)
33
- * help({topic?}) → cold-agent language discovery (read, v0.2.8)
21
+ * LEARN help({topic?}) language quickstart + deep topics
22
+ * runtime_capabilities({include?})→ wired connectors + shell-exec mode
23
+ * DISCOVER skill_list({filter?}) SkillCatalog (grouped by audience; entries carry the full contract)
24
+ * skill_preflight({name}) one skill's contract: takes/returns/requires/touches + approval + lifecycle
25
+ * skill_read({name, version?}) {name, version, status, source} (the body itself)
26
+ * data_read({id, store?}) PortableData | null (direct lookup; no data_write MCP by design)
27
+ * DRAFT lint_skill({source?|name}) tiered diagnostics (inner loop)
28
+ * compile_skill({source?|name, inputs?})→ rendered artifact + errors + exec order (pre-commit)
29
+ * COMMIT skill_write({name, source, overwrite?})→ store the body (write; secured mode forces unsigned→Draft)
30
+ * APPROVE skill_status({name, new_state}) Draft/Approved/Disabled (write; secured promote needs a signature)
31
+ * RUN execute_skill({name|source, inputs?, mechanical?})→ run + return result (write)
32
+ * AUTOMATE register_trigger / list_triggers / set_trigger_enabled / unregister_trigger → autonomous dispatch
33
+ * OBSERVE health_metrics({filter?}) per-skill/connector aggregates
34
+ * blocked_shell_attempts() allowlist-refused shell ops (observe→promote loop)
34
35
  */
35
36
  export interface JsonRpcRequest {
36
37
  jsonrpc: "2.0";
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ import { healthMetrics } from "./metrics.js";
4
4
  import { lint } from "./lint.js";
5
5
  import { compile } from "./compile.js";
6
6
  import { parse as parseSkill } from "./parser.js";
7
+ import { extractEffectfulFootprint } from "./skill-surface.js";
7
8
  import { listKnownConnectorClasses } from "./connectors/config.js";
8
9
  import { LintFailureError, MissingSkillReferenceError, OpError } from "./errors.js";
9
10
  import { executeSkillByName, executeSkillFromSource, RecursionDepthExceededError, SkillNotFoundForCompositionError, } from "./composition.js";
@@ -118,7 +119,7 @@ export class McpServer {
118
119
  registerBuiltinTools() {
119
120
  this.registerTool({
120
121
  name: "skill_list",
121
- description: "Discover skills in the configured SkillStore (v0.9.8). Returns a `SkillCatalog` pre-grouped by audience-derived category: `receives` (skills that push to the calling agent via `# Output: agent:`), `skills` (skills the agent can invoke), `headless` (admin-view only). Category derived from each skill's `# Output:` declarations. Filter by audience / status / trigger_kind / domain_tags / name_prefix / author (AND-composed). Default: audience=\"agent\", status=\"Approved\". v0.18.6 — `author` filter narrows to skills authored by a specific identity; entries carry `author` for substrates that track it (null when not).",
122
+ description: "Discover skills in the configured SkillStore. Returns a `SkillCatalog` pre-grouped by audience-derived category: `receives` (skills that push to the calling agent via `# Output: agent:`), `skills` (skills the agent can invoke), `headless` (admin-view only). Category derived from each skill's `# Output:` declarations. Filter by audience / status / trigger_kind / domain_tags / name_prefix / author (AND-composed). Default: audience=\"agent\", status=\"Approved\". Every entry carries the full preflight contract — `vars` (inputs), `returns` (exported vars), `requires` (capability needs), `effectful_footprint` (which connectors / shell binaries / file + notify ops it touches), plus `gate_ok` (cleared-to-run under the current mode) and `author` (when the substrate tracks it). So you can read a skill's whole I/O + effect contract here without a per-skill `skill_preflight` call; reach for `skill_preflight` when you want one skill's contract + version/lifecycle detail, or `skill_read` for the body.",
122
123
  inputSchema: {
123
124
  type: "object",
124
125
  properties: {
@@ -143,7 +144,7 @@ export class McpServer {
143
144
  });
144
145
  this.registerTool({
145
146
  name: "blocked_shell_attempts",
146
- description: "v0.18.9 — list shell op dispatches refused by the binary allowlist gate. Queries the trace store cross-skill, filters to op records carrying `blocked_reason: \"binary-not-allowed\"`. Returns flat list ready for the dashboard's observe→promote loop (operator sees what binaries skills tried to invoke; decides whether to add any to `SKILLSCRIPT_SHELL_ALLOWLIST`).",
147
+ description: "List shell op dispatches refused by the binary allowlist gate. Queries the trace store cross-skill, filtering to op records carrying `blocked_reason: \"binary-not-allowed\"`. Returns a flat list for the dashboard's observe→promote loop: the operator sees what binaries skills tried to invoke, then decides whether to add any to `SKILLSCRIPT_SHELL_ALLOWLIST`. Read-only.",
147
148
  inputSchema: {
148
149
  type: "object",
149
150
  properties: {
@@ -196,8 +197,8 @@ export class McpServer {
196
197
  },
197
198
  });
198
199
  this.registerTool({
199
- name: "skill_metadata",
200
- description: "Get metadata + version history + recent fires + approval-gate state for a specific skill by name. For the source body itself, call skill_read.",
200
+ name: "skill_preflight",
201
+ description: "PRE-EXECUTION CONTRACT CHECK call this BEFORE executing or composing a skill to see what it takes, returns, requires, and touches: its inputs (vars), exported variables (returns), capability requirements, and effectful footprint (which connectors / shell binaries / write + notify ops it dispatches). Also reports whether it's cleared to run (approval-gate state) + version/lifecycle. The least-privilege checklist for a human approver, and the contract surface for a cold author composing against it. Discover skills with skill_list (its entries already carry this same contract); call skill_preflight for one skill's contract plus version/lifecycle detail, or skill_read for the source body.",
201
202
  inputSchema: {
202
203
  type: "object",
203
204
  properties: {
@@ -219,15 +220,28 @@ export class McpServer {
219
220
  // stale-Approved skills (body edited since approval, token no
220
221
  // longer verifies). `null` when the source isn't loadable.
221
222
  let approval = null;
223
+ // v0.21.0 — the contract surface: what the skill TAKES (vars, via the
224
+ // parser's frontmatter), RETURNS (exported vars), REQUIRES (capability
225
+ // clauses), and TOUCHES (effectful footprint). Derived statically from
226
+ // the body. Null when the source isn't loadable.
227
+ let contract = null;
222
228
  if (loaded?.source !== undefined) {
223
229
  const g = evaluateApprovalGate(loaded.source);
224
230
  approval = g.ok ? { gate_ok: true } : { gate_ok: false, reason: g.reason };
231
+ const parsed = parseSkill(loaded.source);
232
+ contract = {
233
+ vars: parsed.vars.map((v) => v.name),
234
+ returns: parsed.returns,
235
+ requires: parsed.requires,
236
+ effectful_footprint: extractEffectfulFootprint(parsed),
237
+ };
225
238
  }
226
239
  return {
227
240
  metadata,
241
+ contract,
242
+ approval,
228
243
  versions,
229
244
  recent_fires,
230
- approval,
231
245
  };
232
246
  },
233
247
  });
@@ -277,7 +291,7 @@ export class McpServer {
277
291
  });
278
292
  this.registerTool({
279
293
  name: "skill_status",
280
- description: "Transition a skill's status. Valid states: Draft, Approved, Disabled. Write operation.",
294
+ description: "Transition a skill's lifecycle status: Draft, Approved, Disabled. Approved is the only status that executes and lets triggers fire; Draft is the editing state; Disabled retires a skill without deleting it (re-enable by transitioning back to Approved/Draft). In secured mode you CANNOT promote to Approved here without a valid signature — approve via the dashboard or `skillfile approve` (which signs the body); a bare status flip can't grant approval. Write operation.",
281
295
  inputSchema: {
282
296
  type: "object",
283
297
  properties: {
@@ -296,6 +310,19 @@ export class McpServer {
296
310
  if (!isSkillStatus(newState)) {
297
311
  throw new OpError(`\`skill_status\` requires \`new_state\` to be one of ${VALID_SKILL_STATUSES.map((s) => `"${s}"`).join(", ")}; got ${JSON.stringify(newState)}.`, "skill_status", `Pass \`new_state\` as one of: ${VALID_SKILL_STATUSES.join(" | ")}.`, name);
298
312
  }
313
+ // v0.21.0 — store-agnostic secured-mode closure (red-team 33bf53d3).
314
+ // skill_status cannot GRANT approval in secured mode: promotion to
315
+ // Approved is refused unless the stored body already carries a valid v3
316
+ // signature. The per-store guard (FilesystemSkillStore/SqliteSkillStore)
317
+ // didn't cover custom adopter stores (e.g. AMP-backed), so an agent could
318
+ // flip Draft→Approved with no signature — a forgeable trust-state lie.
319
+ // Enforce HERE, at the ingress, regardless of substrate.
320
+ if (newState === "Approved" && isSecuredMode()) {
321
+ const loaded = await this.deps.skillStore.load(name).catch(() => null);
322
+ if (loaded === null || !evaluateApprovalGate(loaded.source).ok) {
323
+ throw new OpError(`cannot promote '${name}' to Approved in secured mode — the skill carries no valid signature. Approve it via the dashboard or \`skillfile approve\` (which signs the body); a status change alone cannot grant approval.`, "skill_status", `Sign the skill with the operator's key, then it can be Approved.`, name);
324
+ }
325
+ }
299
326
  const result = await this.deps.skillStore.update_status(name, newState);
300
327
  // v0.19.1 — sync declarative triggers on status transition.
301
328
  // Approved → register the skill's declared triggers; Draft or
@@ -307,7 +334,7 @@ export class McpServer {
307
334
  });
308
335
  this.registerTool({
309
336
  name: "list_triggers",
310
- description: "List registered triggers. Optionally filter by skill name or trigger source.",
337
+ description: "List registered triggers — the autonomous-dispatch registry. Optionally filter by skill name or trigger source (cron / event). Read-only. Pairs with register_trigger / set_trigger_enabled / unregister_trigger to manage how Approved skills fire on their own.",
311
338
  inputSchema: {
312
339
  type: "object",
313
340
  properties: {
@@ -326,7 +353,7 @@ export class McpServer {
326
353
  });
327
354
  this.registerTool({
328
355
  name: "register_trigger",
329
- description: "Register a new trigger for a skill. Returns the registration. Write operation.",
356
+ description: "Register an autonomous-dispatch trigger for a skill — cron (time-based) or event (HTTP POST /event ingress, named). Only Approved skills fire; the scheduler re-verifies the approval gate at fire time, so registering a trigger never bypasses approval. Skills can also declare triggers inline via `# Triggers:` frontmatter (synced automatically on approval) — use this tool for imperative / dynamic registration. Returns the registration. Write operation.",
330
357
  inputSchema: {
331
358
  type: "object",
332
359
  properties: {
@@ -374,7 +401,7 @@ export class McpServer {
374
401
  });
375
402
  this.registerTool({
376
403
  name: "unregister_trigger",
377
- description: "Unregister a trigger by id. Returns boolean (true if removed, false if id not found). Write operation.",
404
+ description: "Remove a registered trigger by id (see list_triggers for ids). Returns true if removed, false if the id wasn't found. Write operation.",
378
405
  inputSchema: {
379
406
  type: "object",
380
407
  properties: { trigger_id: { type: "string" } },
@@ -387,7 +414,7 @@ export class McpServer {
387
414
  });
388
415
  this.registerTool({
389
416
  name: "set_trigger_enabled",
390
- description: "v0.9.0 — toggle a trigger's enabled state. Disabled triggers stay registered but the scheduler skips firing them (vacation / maintenance windows). State persists via the onTriggersChanged hook for imperative triggers. Returns the updated registration, or null if no trigger has that id. Write operation.",
417
+ description: "Toggle a trigger's enabled state without unregistering it — disabled triggers stay in the registry but the scheduler skips firing them (vacation / maintenance windows). State persists via the onTriggersChanged hook for imperative triggers. Returns the updated registration, or null if no trigger has that id. Write operation.",
391
418
  inputSchema: {
392
419
  type: "object",
393
420
  properties: {
@@ -449,7 +476,7 @@ export class McpServer {
449
476
  // ─── v0.2.3 — over-the-wire authoring lifecycle ────────────────────────
450
477
  this.registerTool({
451
478
  name: "lint_skill",
452
- description: "Run static lint against a skill source body or stored skill name. Returns diagnostics across tier-1 (errors that block compile), tier-2 (warnings), tier-3 (advisories). Read-only. Inner-loop affordance for cold authors iterating on a draft.",
479
+ description: "Run static lint against a skill source body or stored skill name. Returns diagnostics across tier-1 (errors that block compile), tier-2 (warnings), tier-3 (advisories). Read-only. The inner-loop affordance while drafting — lint as you iterate, then compile_skill for the full pre-commit check before skill_write.",
453
480
  inputSchema: {
454
481
  type: "object",
455
482
  properties: {
@@ -461,7 +488,7 @@ export class McpServer {
461
488
  });
462
489
  this.registerTool({
463
490
  name: "compile_skill",
464
- description: "Compile a skill source body or stored skill name. Returns the rendered artifact + parse/compile errors + resolved variables + topological execution order. Read-only. Pre-commit validation affordance.",
491
+ description: "Compile a skill source body or stored skill name. Returns the rendered artifact + parse/compile errors + resolved variables + topological execution order. Read-only. The pre-commit check after lint_skill passes and before skill_write — confirms the body compiles and shows the execution order without running effects (use execute_skill `mechanical: true` for a no-fire dispatch preview).",
465
492
  inputSchema: {
466
493
  type: "object",
467
494
  properties: {
@@ -478,7 +505,7 @@ export class McpServer {
478
505
  });
479
506
  this.registerTool({
480
507
  name: "skill_write",
481
- description: "Write a skill body into the configured SkillStore. Tier-1 lint runs at write time (SkillStore contract); throws on rejection. Returns version + content_hash. The `# Status:` header in the source body is honored — write with `# Status: Approved` and the skill lands Approved (otherwise defaults to Draft). Promote/demote later via skill_status. Write operation.",
508
+ description: "Write a skill body into the configured SkillStore. Tier-1 lint runs at write time (SkillStore contract) and throws on rejection run lint_skill / compile_skill first to iterate cleanly. The `# Status:` header is honored: `# Status: Approved` lands Approved in unsecured mode, but in secured mode an unsigned body is forced to Draft (a write can't grant approval). Promote later via skill_status, or the dashboard / `skillfile approve` in secured mode. Returns version + content_hash. Write operation.",
482
509
  inputSchema: {
483
510
  type: "object",
484
511
  properties: {
@@ -533,7 +560,7 @@ export class McpServer {
533
560
  async executeSkill(args, callerCtx = {}) {
534
561
  // v0.15.2 — `name` is the canonical kwarg; `skill_name` is a silent
535
562
  // back-compat alias. Aligns the surface with the other skill_* tools
536
- // (skill_read / skill_metadata / skill_status / skill_write all take
563
+ // (skill_read / skill_preflight / skill_status / skill_write all take
537
564
  // `name`). Per Perry signoff (thread 75abc8c0): silent alias — no
538
565
  // tier-3 advisory, no deprecation warn. If both are supplied with
539
566
  // different values, that's ambiguous; reject so the caller picks one.
@@ -817,7 +844,21 @@ export class McpServer {
817
844
  // posture (every write requires explicit human promotion regardless
818
845
  // of body claim) opt in via the flag at runtime startup. Per Perry's
819
846
  // `787b6b95` Option A.
820
- const bodyToStore = this.deps.forceAlwaysDraft === true ? forceDraftStatus(source) : source;
847
+ // v0.21.0 store-agnostic secured-mode closure (red-team 33bf53d3).
848
+ // skill_write cannot GRANT approval: if the body claims Approved without a
849
+ // valid v3 signature, force Draft — regardless of the SkillStore impl. The
850
+ // per-store guard didn't cover custom adopter stores (AMP-backed), letting an
851
+ // agent write status=Approved with no signature (a forgeable trust-state lie
852
+ // that misleads skill_list / skill_preflight / the dashboard). evaluateApproval
853
+ // -Gate is not-ok for any Draft OR unsigned-Approved body, so a genuine
854
+ // approve-flow write (signed body) is honored; everything else lands Draft.
855
+ let bodyToStore = source;
856
+ if (this.deps.forceAlwaysDraft === true) {
857
+ bodyToStore = forceDraftStatus(source);
858
+ }
859
+ else if (isSecuredMode() && !evaluateApprovalGate(source).ok) {
860
+ bodyToStore = forceDraftStatus(source);
861
+ }
821
862
  // v0.17.0 — thread host-attested caller identity into `store({author})`
822
863
  // so MCP-authored skills stamp `SkillMeta.author = <calling agent>`,
823
864
  // not the runtime's wiring identity. When ctx.callerIdentity is