skillscript-runtime 0.22.1 → 0.23.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/connectors/http-mcp.d.ts +9 -1
  3. package/dist/connectors/http-mcp.d.ts.map +1 -1
  4. package/dist/connectors/http-mcp.js +26 -5
  5. package/dist/connectors/http-mcp.js.map +1 -1
  6. package/dist/connectors/mcp-remote.d.ts +10 -1
  7. package/dist/connectors/mcp-remote.d.ts.map +1 -1
  8. package/dist/connectors/mcp-remote.js +23 -3
  9. package/dist/connectors/mcp-remote.js.map +1 -1
  10. package/dist/connectors/types.d.ts +21 -0
  11. package/dist/connectors/types.d.ts.map +1 -1
  12. package/dist/connectors/types.js.map +1 -1
  13. package/dist/lint.d.ts +17 -1
  14. package/dist/lint.d.ts.map +1 -1
  15. package/dist/lint.js +167 -0
  16. package/dist/lint.js.map +1 -1
  17. package/dist/mcp-server.d.ts +7 -0
  18. package/dist/mcp-server.d.ts.map +1 -1
  19. package/dist/mcp-server.js +86 -2
  20. package/dist/mcp-server.js.map +1 -1
  21. package/dist/observed-shape.d.ts +36 -0
  22. package/dist/observed-shape.d.ts.map +1 -0
  23. package/dist/observed-shape.js +67 -0
  24. package/dist/observed-shape.js.map +1 -0
  25. package/dist/runtime.d.ts.map +1 -1
  26. package/dist/runtime.js +29 -0
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/skill-surface.d.ts +12 -0
  29. package/dist/skill-surface.d.ts.map +1 -1
  30. package/dist/skill-surface.js +22 -0
  31. package/dist/skill-surface.js.map +1 -1
  32. package/dist/trace.d.ts +28 -0
  33. package/dist/trace.d.ts.map +1 -1
  34. package/dist/trace.js +56 -3
  35. package/dist/trace.js.map +1 -1
  36. package/docs/adopter-playbook.md +10 -0
  37. package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
  38. package/package.json +1 -1
package/dist/lint.js CHANGED
@@ -24,6 +24,7 @@ export async function lint(source, options) {
24
24
  bareArrayReturnTools: options?.bareArrayReturnTools ?? [],
25
25
  shellAllowlist: options?.shellAllowlist,
26
26
  mcpConnectorStaticTools: collectMcpConnectorStaticToolsFromRegistry(options?.registry),
27
+ mcpConnectorToolSchemas: options?.mcpConnectorToolSchemas ?? (await collectMcpConnectorToolSchemasFromRegistry(options?.registry)),
27
28
  };
28
29
  const findings = [];
29
30
  for (const rule of RULES) {
@@ -67,6 +68,9 @@ export function lintSync(source, options) {
67
68
  bareArrayReturnTools: options?.bareArrayReturnTools ?? [],
68
69
  shellAllowlist: options?.shellAllowlist,
69
70
  mcpConnectorStaticTools: collectMcpConnectorStaticToolsFromRegistry(options?.registry),
71
+ // lintSync can't warm schemas (describeTools is async); honor an explicit
72
+ // injection (tests) but otherwise leave empty → connector-arg rules degrade.
73
+ mcpConnectorToolSchemas: options?.mcpConnectorToolSchemas ?? new Map(),
70
74
  };
71
75
  const findings = [];
72
76
  for (const rule of RULES) {
@@ -1037,6 +1041,124 @@ const UNVERIFIED_QUALIFIED_TOOL = {
1037
1041
  return findings;
1038
1042
  },
1039
1043
  };
1044
+ // v0.23.0 — connector-aware input lint. For a qualified `$ ref.tool arg=...`
1045
+ // op where the connector's warmed inputSchema is available (ctx.
1046
+ // mcpConnectorToolSchemas), validate the arg NAMES against the schema. Catches
1047
+ // the typo class (`$ ddg.search querry=...`) statically — today it compiles
1048
+ // clean and fails only at runtime because MCP connector args are unvalidated.
1049
+ //
1050
+ // SCOPE (phase 1): qualified `$ connector.tool` form only (bare `$ tool`
1051
+ // resolves its owning connector at dispatch and is deferred). Reads only the
1052
+ // pre-warmed cache — no I/O here; the async lint() entry point warms it.
1053
+ //
1054
+ // GRACEFUL DEGRADE: no cache entry / no inputSchema / open schema
1055
+ // (additionalProperties: true) → skip. Schema-less or unreachable connectors
1056
+ // behave exactly as before (no false positives).
1057
+ function connectorArgLintInfo(ctx, op) {
1058
+ if (op.kind !== "$" || op.mcpConnector === undefined)
1059
+ return null;
1060
+ const byTool = ctx.mcpConnectorToolSchemas.get(op.mcpConnector);
1061
+ if (byTool === undefined)
1062
+ return null;
1063
+ const m = /^([A-Za-z_][\w:-]*)\s*([\s\S]*)$/.exec(op.body);
1064
+ if (m === null)
1065
+ return null;
1066
+ const toolName = m[1];
1067
+ const descriptor = byTool.get(toolName);
1068
+ if (descriptor === undefined || descriptor.inputSchema === undefined)
1069
+ return null;
1070
+ const argNames = [];
1071
+ for (const tok of tokenizeKeywordArgs(m[2] ?? "")) {
1072
+ const eq = tok.indexOf("=");
1073
+ if (eq === -1)
1074
+ continue;
1075
+ const k = tok.slice(0, eq).trim();
1076
+ // `timeout` is a runtime-reserved per-op kwarg, popped before the connector
1077
+ // sees args — never part of the tool's own schema.
1078
+ if (k !== "" && k !== "timeout")
1079
+ argNames.push(k);
1080
+ }
1081
+ return { connector: op.mcpConnector, toolName, argNames, inputSchema: descriptor.inputSchema };
1082
+ }
1083
+ const UNKNOWN_CONNECTOR_ARG = {
1084
+ id: "unknown-connector-arg",
1085
+ severity: "warning",
1086
+ description: "A qualified `$ ref.tool arg=...` op passes an argument name the connector tool's inputSchema doesn't declare.",
1087
+ remediation: "Use an argument the tool declares (see `runtime_capabilities` for the connector tool's schema), or remove the unrecognized one. If the tool genuinely accepts it, the upstream inputSchema may be incomplete — verify before relying on it.",
1088
+ check: (ctx) => {
1089
+ if (ctx.mcpConnectorToolSchemas.size === 0)
1090
+ return [];
1091
+ const findings = [];
1092
+ const reported = new Set();
1093
+ for (const [targetName, target] of ctx.parsed.targets) {
1094
+ walkOps(target.ops, (op) => {
1095
+ const info = connectorArgLintInfo(ctx, op);
1096
+ if (info === null)
1097
+ return;
1098
+ const props = info.inputSchema.properties;
1099
+ if (props === null || typeof props !== "object")
1100
+ return; // can't enumerate the valid set
1101
+ if (info.inputSchema.additionalProperties === true)
1102
+ return; // open schema
1103
+ const valid = new Set(Object.keys(props));
1104
+ for (const arg of info.argNames) {
1105
+ if (valid.has(arg))
1106
+ continue;
1107
+ const key = `${targetName}:${info.connector}:${info.toolName}:${arg}`;
1108
+ if (reported.has(key))
1109
+ continue;
1110
+ reported.add(key);
1111
+ findings.push({
1112
+ rule: "unknown-connector-arg",
1113
+ severity: "warning",
1114
+ message: `\`$ ${info.connector}.${info.toolName}\` in target '${targetName}' passes arg '${arg}', which isn't in the tool's input schema. Declared args: ${[...valid].join(", ") || "(none)"}.`,
1115
+ block: targetName,
1116
+ extras: { connector: info.connector, tool: info.toolName, arg, declared_args: [...valid] },
1117
+ });
1118
+ }
1119
+ });
1120
+ }
1121
+ return findings;
1122
+ },
1123
+ };
1124
+ const MISSING_REQUIRED_CONNECTOR_ARG = {
1125
+ id: "missing-required-connector-arg",
1126
+ severity: "warning",
1127
+ description: "A qualified `$ ref.tool` op omits an argument the connector tool's inputSchema marks required.",
1128
+ remediation: "Supply the required argument(s). See `runtime_capabilities` for the tool's schema (its `required` list).",
1129
+ check: (ctx) => {
1130
+ if (ctx.mcpConnectorToolSchemas.size === 0)
1131
+ return [];
1132
+ const findings = [];
1133
+ const reported = new Set();
1134
+ for (const [targetName, target] of ctx.parsed.targets) {
1135
+ walkOps(target.ops, (op) => {
1136
+ const info = connectorArgLintInfo(ctx, op);
1137
+ if (info === null)
1138
+ return;
1139
+ const requiredRaw = info.inputSchema.required;
1140
+ if (!Array.isArray(requiredRaw))
1141
+ return;
1142
+ const provided = new Set(info.argNames);
1143
+ const missing = requiredRaw.filter((r) => typeof r === "string" && !provided.has(r));
1144
+ if (missing.length === 0)
1145
+ return;
1146
+ const key = `${targetName}:${info.connector}:${info.toolName}:${missing.join(",")}`;
1147
+ if (reported.has(key))
1148
+ return;
1149
+ reported.add(key);
1150
+ findings.push({
1151
+ rule: "missing-required-connector-arg",
1152
+ severity: "warning",
1153
+ message: `\`$ ${info.connector}.${info.toolName}\` in target '${targetName}' is missing required arg(s): ${missing.join(", ")}.`,
1154
+ block: targetName,
1155
+ extras: { connector: info.connector, tool: info.toolName, missing },
1156
+ });
1157
+ });
1158
+ }
1159
+ return findings;
1160
+ },
1161
+ };
1040
1162
  // v0.5.0 item 5 — bare `$ TOOL` op (no connector prefix) when no
1041
1163
  // `primary` connector is wired. Runtime now throws ConnectorNotFoundError
1042
1164
  // instead of silent-stub (was: emitted "Would call tool X" + bound null,
@@ -3271,6 +3393,8 @@ const RULES = [
3271
3393
  DISALLOWED_TOOL,
3272
3394
  UNKNOWN_TOOL_ON_CONNECTOR,
3273
3395
  UNVERIFIED_QUALIFIED_TOOL,
3396
+ UNKNOWN_CONNECTOR_ARG,
3397
+ MISSING_REQUIRED_CONNECTOR_ARG,
3274
3398
  UNINITIALIZED_APPEND,
3275
3399
  FOREACH_LOCAL_ACCUMULATOR_TARGET,
3276
3400
  APPEND_TO_NON_LIST,
@@ -3544,6 +3668,49 @@ function collectMcpConnectorStaticToolsFromRegistry(registry) {
3544
3668
  }
3545
3669
  return out;
3546
3670
  }
3671
+ /**
3672
+ * v0.23.0 — warm + collect per-connector tool schemas for connector-aware
3673
+ * input lint. For each wired MCP connector exposing `describeTools()`, calls it
3674
+ * (which ensure-warms a read-only `tools/list` when cold — protocol
3675
+ * introspection, NOT a tool dispatch, so no effect boundary is crossed) under a
3676
+ * short timeout. Best-effort + graceful: a connector with no `describeTools()`,
3677
+ * an unreachable upstream, or a timeout simply contributes no schema, and the
3678
+ * arg rules stay silent for it (no false positives). Async — only reachable via
3679
+ * the async `lint()` entry point.
3680
+ */
3681
+ const CONNECTOR_SCHEMA_WARM_TIMEOUT_MS = 2000;
3682
+ async function collectMcpConnectorToolSchemasFromRegistry(registry) {
3683
+ const out = new Map();
3684
+ if (registry === undefined)
3685
+ return out;
3686
+ await Promise.all(registry.listMcpConnectors().map(async (e) => {
3687
+ const instance = e.instance;
3688
+ if (typeof instance.describeTools !== "function")
3689
+ return;
3690
+ try {
3691
+ const descriptors = await Promise.race([
3692
+ instance.describeTools(),
3693
+ new Promise((_, reject) => setTimeout(() => reject(new Error("describeTools timeout")), CONNECTOR_SCHEMA_WARM_TIMEOUT_MS)),
3694
+ ]);
3695
+ // Respect the operator's per-connector `allowed_tools` gate: don't
3696
+ // surface (or validate against) tools the operator gated off — a
3697
+ // disallowed `$ conn.tool` is the `disallowed-tool` rule's job, not
3698
+ // ours. undefined allowlist = allow-all.
3699
+ const allowed = e.allowedTools;
3700
+ const byTool = new Map();
3701
+ for (const d of descriptors) {
3702
+ if (allowed !== undefined && !allowed.includes(d.name))
3703
+ continue;
3704
+ byTool.set(d.name, d);
3705
+ }
3706
+ out.set(e.name, byTool);
3707
+ }
3708
+ catch {
3709
+ // Unreachable / timeout / no schema — degrade arg-agnostic for this connector.
3710
+ }
3711
+ }));
3712
+ return out;
3713
+ }
3547
3714
  function buildFeatureSet(classes) {
3548
3715
  const provided = new Set();
3549
3716
  for (const Ctor of classes) {