skillscript-runtime 0.22.1 → 0.23.1

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 +20 -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 +15 -1
  7. package/dist/connectors/mcp-remote.d.ts.map +1 -1
  8. package/dist/connectors/mcp-remote.js +74 -9
  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 +177 -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) {
@@ -1021,6 +1025,16 @@ const UNVERIFIED_QUALIFIED_TOOL = {
1021
1025
  if (m === null)
1022
1026
  return;
1023
1027
  const toolName = m[1];
1028
+ // v0.23.1 — if the connector's warmed `describeTools` schema verifies
1029
+ // THIS tool (the same source the tier-2 connector-arg rules consume),
1030
+ // the surface IS known — the "can't validate statically" advisory is
1031
+ // stale and contradicts tier-2 ("you passed an unknown arg" + "I can't
1032
+ // see this tool" on the same op). Skip it. Tier-3 still fires when
1033
+ // neither a static nor a warmed dynamic surface verifies the tool
1034
+ // (truly-opaque connector: describeTools absent/unreachable, or a
1035
+ // misspelled tool name not in the fetched surface).
1036
+ if (ctx.mcpConnectorToolSchemas.get(ref)?.has(toolName))
1037
+ return;
1024
1038
  const key = `${targetName}:${ref}:${toolName}`;
1025
1039
  if (reported.has(key))
1026
1040
  return;
@@ -1037,6 +1051,124 @@ const UNVERIFIED_QUALIFIED_TOOL = {
1037
1051
  return findings;
1038
1052
  },
1039
1053
  };
1054
+ // v0.23.0 — connector-aware input lint. For a qualified `$ ref.tool arg=...`
1055
+ // op where the connector's warmed inputSchema is available (ctx.
1056
+ // mcpConnectorToolSchemas), validate the arg NAMES against the schema. Catches
1057
+ // the typo class (`$ ddg.search querry=...`) statically — today it compiles
1058
+ // clean and fails only at runtime because MCP connector args are unvalidated.
1059
+ //
1060
+ // SCOPE (phase 1): qualified `$ connector.tool` form only (bare `$ tool`
1061
+ // resolves its owning connector at dispatch and is deferred). Reads only the
1062
+ // pre-warmed cache — no I/O here; the async lint() entry point warms it.
1063
+ //
1064
+ // GRACEFUL DEGRADE: no cache entry / no inputSchema / open schema
1065
+ // (additionalProperties: true) → skip. Schema-less or unreachable connectors
1066
+ // behave exactly as before (no false positives).
1067
+ function connectorArgLintInfo(ctx, op) {
1068
+ if (op.kind !== "$" || op.mcpConnector === undefined)
1069
+ return null;
1070
+ const byTool = ctx.mcpConnectorToolSchemas.get(op.mcpConnector);
1071
+ if (byTool === undefined)
1072
+ return null;
1073
+ const m = /^([A-Za-z_][\w:-]*)\s*([\s\S]*)$/.exec(op.body);
1074
+ if (m === null)
1075
+ return null;
1076
+ const toolName = m[1];
1077
+ const descriptor = byTool.get(toolName);
1078
+ if (descriptor === undefined || descriptor.inputSchema === undefined)
1079
+ return null;
1080
+ const argNames = [];
1081
+ for (const tok of tokenizeKeywordArgs(m[2] ?? "")) {
1082
+ const eq = tok.indexOf("=");
1083
+ if (eq === -1)
1084
+ continue;
1085
+ const k = tok.slice(0, eq).trim();
1086
+ // `timeout` is a runtime-reserved per-op kwarg, popped before the connector
1087
+ // sees args — never part of the tool's own schema.
1088
+ if (k !== "" && k !== "timeout")
1089
+ argNames.push(k);
1090
+ }
1091
+ return { connector: op.mcpConnector, toolName, argNames, inputSchema: descriptor.inputSchema };
1092
+ }
1093
+ const UNKNOWN_CONNECTOR_ARG = {
1094
+ id: "unknown-connector-arg",
1095
+ severity: "warning",
1096
+ description: "A qualified `$ ref.tool arg=...` op passes an argument name the connector tool's inputSchema doesn't declare.",
1097
+ 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.",
1098
+ check: (ctx) => {
1099
+ if (ctx.mcpConnectorToolSchemas.size === 0)
1100
+ return [];
1101
+ const findings = [];
1102
+ const reported = new Set();
1103
+ for (const [targetName, target] of ctx.parsed.targets) {
1104
+ walkOps(target.ops, (op) => {
1105
+ const info = connectorArgLintInfo(ctx, op);
1106
+ if (info === null)
1107
+ return;
1108
+ const props = info.inputSchema.properties;
1109
+ if (props === null || typeof props !== "object")
1110
+ return; // can't enumerate the valid set
1111
+ if (info.inputSchema.additionalProperties === true)
1112
+ return; // open schema
1113
+ const valid = new Set(Object.keys(props));
1114
+ for (const arg of info.argNames) {
1115
+ if (valid.has(arg))
1116
+ continue;
1117
+ const key = `${targetName}:${info.connector}:${info.toolName}:${arg}`;
1118
+ if (reported.has(key))
1119
+ continue;
1120
+ reported.add(key);
1121
+ findings.push({
1122
+ rule: "unknown-connector-arg",
1123
+ severity: "warning",
1124
+ 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)"}.`,
1125
+ block: targetName,
1126
+ extras: { connector: info.connector, tool: info.toolName, arg, declared_args: [...valid] },
1127
+ });
1128
+ }
1129
+ });
1130
+ }
1131
+ return findings;
1132
+ },
1133
+ };
1134
+ const MISSING_REQUIRED_CONNECTOR_ARG = {
1135
+ id: "missing-required-connector-arg",
1136
+ severity: "warning",
1137
+ description: "A qualified `$ ref.tool` op omits an argument the connector tool's inputSchema marks required.",
1138
+ remediation: "Supply the required argument(s). See `runtime_capabilities` for the tool's schema (its `required` list).",
1139
+ check: (ctx) => {
1140
+ if (ctx.mcpConnectorToolSchemas.size === 0)
1141
+ return [];
1142
+ const findings = [];
1143
+ const reported = new Set();
1144
+ for (const [targetName, target] of ctx.parsed.targets) {
1145
+ walkOps(target.ops, (op) => {
1146
+ const info = connectorArgLintInfo(ctx, op);
1147
+ if (info === null)
1148
+ return;
1149
+ const requiredRaw = info.inputSchema.required;
1150
+ if (!Array.isArray(requiredRaw))
1151
+ return;
1152
+ const provided = new Set(info.argNames);
1153
+ const missing = requiredRaw.filter((r) => typeof r === "string" && !provided.has(r));
1154
+ if (missing.length === 0)
1155
+ return;
1156
+ const key = `${targetName}:${info.connector}:${info.toolName}:${missing.join(",")}`;
1157
+ if (reported.has(key))
1158
+ return;
1159
+ reported.add(key);
1160
+ findings.push({
1161
+ rule: "missing-required-connector-arg",
1162
+ severity: "warning",
1163
+ message: `\`$ ${info.connector}.${info.toolName}\` in target '${targetName}' is missing required arg(s): ${missing.join(", ")}.`,
1164
+ block: targetName,
1165
+ extras: { connector: info.connector, tool: info.toolName, missing },
1166
+ });
1167
+ });
1168
+ }
1169
+ return findings;
1170
+ },
1171
+ };
1040
1172
  // v0.5.0 item 5 — bare `$ TOOL` op (no connector prefix) when no
1041
1173
  // `primary` connector is wired. Runtime now throws ConnectorNotFoundError
1042
1174
  // instead of silent-stub (was: emitted "Would call tool X" + bound null,
@@ -3271,6 +3403,8 @@ const RULES = [
3271
3403
  DISALLOWED_TOOL,
3272
3404
  UNKNOWN_TOOL_ON_CONNECTOR,
3273
3405
  UNVERIFIED_QUALIFIED_TOOL,
3406
+ UNKNOWN_CONNECTOR_ARG,
3407
+ MISSING_REQUIRED_CONNECTOR_ARG,
3274
3408
  UNINITIALIZED_APPEND,
3275
3409
  FOREACH_LOCAL_ACCUMULATOR_TARGET,
3276
3410
  APPEND_TO_NON_LIST,
@@ -3544,6 +3678,49 @@ function collectMcpConnectorStaticToolsFromRegistry(registry) {
3544
3678
  }
3545
3679
  return out;
3546
3680
  }
3681
+ /**
3682
+ * v0.23.0 — warm + collect per-connector tool schemas for connector-aware
3683
+ * input lint. For each wired MCP connector exposing `describeTools()`, calls it
3684
+ * (which ensure-warms a read-only `tools/list` when cold — protocol
3685
+ * introspection, NOT a tool dispatch, so no effect boundary is crossed) under a
3686
+ * short timeout. Best-effort + graceful: a connector with no `describeTools()`,
3687
+ * an unreachable upstream, or a timeout simply contributes no schema, and the
3688
+ * arg rules stay silent for it (no false positives). Async — only reachable via
3689
+ * the async `lint()` entry point.
3690
+ */
3691
+ const CONNECTOR_SCHEMA_WARM_TIMEOUT_MS = 2000;
3692
+ async function collectMcpConnectorToolSchemasFromRegistry(registry) {
3693
+ const out = new Map();
3694
+ if (registry === undefined)
3695
+ return out;
3696
+ await Promise.all(registry.listMcpConnectors().map(async (e) => {
3697
+ const instance = e.instance;
3698
+ if (typeof instance.describeTools !== "function")
3699
+ return;
3700
+ try {
3701
+ const descriptors = await Promise.race([
3702
+ instance.describeTools(),
3703
+ new Promise((_, reject) => setTimeout(() => reject(new Error("describeTools timeout")), CONNECTOR_SCHEMA_WARM_TIMEOUT_MS)),
3704
+ ]);
3705
+ // Respect the operator's per-connector `allowed_tools` gate: don't
3706
+ // surface (or validate against) tools the operator gated off — a
3707
+ // disallowed `$ conn.tool` is the `disallowed-tool` rule's job, not
3708
+ // ours. undefined allowlist = allow-all.
3709
+ const allowed = e.allowedTools;
3710
+ const byTool = new Map();
3711
+ for (const d of descriptors) {
3712
+ if (allowed !== undefined && !allowed.includes(d.name))
3713
+ continue;
3714
+ byTool.set(d.name, d);
3715
+ }
3716
+ out.set(e.name, byTool);
3717
+ }
3718
+ catch {
3719
+ // Unreachable / timeout / no schema — degrade arg-agnostic for this connector.
3720
+ }
3721
+ }));
3722
+ return out;
3723
+ }
3547
3724
  function buildFeatureSet(classes) {
3548
3725
  const provided = new Set();
3549
3726
  for (const Ctor of classes) {