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.
- package/README.md +37 -10
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/skill-store-mcp.js +1 -1
- package/dist/connectors/skill-store-mcp.js.map +1 -1
- package/dist/connectors/skill-store.js +1 -1
- package/dist/connectors/skill-store.js.map +1 -1
- package/dist/connectors/types.d.ts +32 -1
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/types.js.map +1 -1
- package/dist/dashboard/server.d.ts +43 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +166 -2
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/spa/app.js +142 -26
- package/dist/mcp-server.d.ts +17 -16
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +56 -15
- package/dist/mcp-server.js.map +1 -1
- package/dist/skill-catalog.d.ts +2 -10
- package/dist/skill-catalog.d.ts.map +1 -1
- package/dist/skill-catalog.js +15 -0
- package/dist/skill-catalog.js.map +1 -1
- package/dist/skill-surface.d.ts +19 -0
- package/dist/skill-surface.d.ts.map +1 -0
- package/dist/skill-surface.js +86 -0
- package/dist/skill-surface.js.map +1 -0
- package/docs/adopter-playbook.md +17 -8
- package/docs/configuration.md +9 -3
- package/docs/connector-contract-reference.md +4 -6
- package/docs/sqlite-skill-store.md +5 -7
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/package.json +1 -1
- package/scaffold/.env.example +50 -8
|
@@ -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 [
|
|
49
|
-
// v0.
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
50
|
+
const [draftCat, apprCat, disabledCat, triggers, metrics, capabilities, blocked] = await Promise.all([
|
|
51
|
+
// v0.20.2 — fetch 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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 <name></code> at a terminal).`
|
|
132
|
+
: `Approval requires the operator's key: <code>skillfile approve <name></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.
|
|
125
|
-
<code>skillfile approve <name></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
|
-
|
|
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 ?
|
|
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
|
|
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
|
|
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("
|
|
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">${
|
|
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("
|
|
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
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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";
|
package/dist/mcp-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkE,MAAM,uBAAuB,CAAC;AAGxH,OAAO,KAAK,EAAE,SAAS,EAAgD,MAAM,gBAAgB,CAAC;AAC9F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
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: "
|
|
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: "
|
|
200
|
-
description: "
|
|
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
|
|
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
|
|
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: "
|
|
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: "
|
|
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.
|
|
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.
|
|
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)
|
|
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 /
|
|
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
|
-
|
|
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
|