skillscript-runtime 0.24.0 → 0.25.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.
- package/CHANGELOG.md +18 -0
- package/README.md +2 -1
- package/UPGRADING.md +9 -1
- package/dist/bootstrap.d.ts +10 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +7 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/types.js.map +1 -1
- package/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +3 -0
- package/dist/help-content.js.map +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +168 -0
- package/dist/lint.js.map +1 -1
- package/dist/mcp-server.d.ts +8 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +22 -6
- package/dist/parser.js.map +1 -1
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +147 -24
- package/dist/runtime.js.map +1 -1
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +3 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/secrets.d.ts +124 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +161 -0
- package/dist/secrets.js.map +1 -0
- package/dist/skill-catalog.d.ts.map +1 -1
- package/dist/skill-catalog.js +2 -0
- package/dist/skill-catalog.js.map +1 -1
- package/docs/adopter-playbook.md +41 -0
- package/docs/language-reference.md +82 -5
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/package.json +1 -1
- package/scaffold/.env.example +13 -0
package/dist/lint.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,WAAW,EAAgB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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"}
|
package/dist/lint.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parse, tokenizeKeywordArgs } from "./parser.js";
|
|
2
|
+
import { extractSecretRefs, findMalformedSecretMarkers, countSecretMarkers } from "./secrets.js";
|
|
2
3
|
import { classifyMutation, authorizationGranted } from "./mutation-gate.js";
|
|
3
4
|
import { KNOWN_FILTERS } from "./filters.js";
|
|
4
5
|
// ─── lint() entry point ────────────────────────────────────────────────────
|
|
@@ -1458,6 +1459,170 @@ const CREDENTIAL_IN_ARGS = {
|
|
|
1458
1459
|
return findings;
|
|
1459
1460
|
},
|
|
1460
1461
|
};
|
|
1462
|
+
// ─── secret references (v0.25.0) ──────────────────────────────────────────
|
|
1463
|
+
// A `{{secret.NAME}}` marker is USE-ONLY: legal only in a sink op (a
|
|
1464
|
+
// `shell(...)` op or a `$ connector.tool` dispatch), never where its value
|
|
1465
|
+
// could be read, bound, or emitted. These two tier-1 rules enforce the
|
|
1466
|
+
// contract at compile time; the runtime resolves markers at the sink
|
|
1467
|
+
// (src/secrets.ts holds SECRET_SINK_OP_KINDS, the shared sink list).
|
|
1468
|
+
/** True when this `$` op is an unqualified built-in intercept (execute_skill /
|
|
1469
|
+
* json_parse) — a `$`-kind op that is NOT a real external sink. A secret
|
|
1470
|
+
* marker in it would flow into a child skill's scope or a parsed/bound var,
|
|
1471
|
+
* never an external credential, so it's a use-only violation. A *qualified*
|
|
1472
|
+
* `$ conn.execute_skill` is a real connector dispatch and not excluded. */
|
|
1473
|
+
function isBuiltinNonSinkDollar(op) {
|
|
1474
|
+
if (op.kind !== "$" || op.mcpConnector !== undefined)
|
|
1475
|
+
return false;
|
|
1476
|
+
const tool = /^([A-Za-z_][\w:-]*)/.exec(op.body.trimStart())?.[1];
|
|
1477
|
+
return tool === "execute_skill" || tool === "json_parse";
|
|
1478
|
+
}
|
|
1479
|
+
/** Text positions of `op` where a secret marker is a legal sink injection. */
|
|
1480
|
+
function secretSinkText(op) {
|
|
1481
|
+
if (op.kind === "shell")
|
|
1482
|
+
return `${op.body ?? ""} ${op.argv?.join(" ") ?? ""}`;
|
|
1483
|
+
if (op.kind === "$" && !isBuiltinNonSinkDollar(op))
|
|
1484
|
+
return op.body ?? "";
|
|
1485
|
+
return "";
|
|
1486
|
+
}
|
|
1487
|
+
/** Text positions of `op` where a secret marker is ILLEGAL (would expose the
|
|
1488
|
+
* value): every author-controlled field except the sink positions above —
|
|
1489
|
+
* the body of a non-sink op, `$set`/`$append` values, foreach lists, op
|
|
1490
|
+
* fallbacks, and condition expressions. */
|
|
1491
|
+
function secretNonSinkText(op) {
|
|
1492
|
+
const parts = [];
|
|
1493
|
+
const bodyIsSink = op.kind === "shell" || (op.kind === "$" && !isBuiltinNonSinkDollar(op));
|
|
1494
|
+
if (!bodyIsSink && op.body)
|
|
1495
|
+
parts.push(op.body);
|
|
1496
|
+
if (op.setValue !== undefined)
|
|
1497
|
+
parts.push(op.setValue);
|
|
1498
|
+
if (op.foreachList !== undefined)
|
|
1499
|
+
parts.push(op.foreachList);
|
|
1500
|
+
if (op.fallback !== undefined)
|
|
1501
|
+
parts.push(op.fallback);
|
|
1502
|
+
if (op.ifBranches !== undefined)
|
|
1503
|
+
for (const b of op.ifBranches)
|
|
1504
|
+
parts.push(b.cond);
|
|
1505
|
+
return parts.join(" ");
|
|
1506
|
+
}
|
|
1507
|
+
const SECRET_USE_ONLY = {
|
|
1508
|
+
id: "secret-use-only",
|
|
1509
|
+
severity: "error",
|
|
1510
|
+
description: "A `{{secret.NAME}}` marker appears outside a secret sink (a `shell(...)` op or a `$ connector.tool` dispatch). Secrets are use-only: the runtime injects the value at the sink and never lets it reach a readable, bindable, or emittable surface.",
|
|
1511
|
+
remediation: "Use the secret only inside a `shell(command=\"... {{secret.NAME}} ...\")` op or a `$ connector.tool arg={{secret.NAME}}` dispatch. Never put it in `$set`, `emit`, a condition, a `# Output:` template, a `file_*`/`notify` op, or an op `(fallback: ...)` — those would expose the value.",
|
|
1512
|
+
check: (ctx) => {
|
|
1513
|
+
const findings = [];
|
|
1514
|
+
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1515
|
+
walkOps(target.ops, (op) => {
|
|
1516
|
+
for (const name of extractSecretRefs(secretNonSinkText(op))) {
|
|
1517
|
+
findings.push({
|
|
1518
|
+
rule: "secret-use-only",
|
|
1519
|
+
severity: "error",
|
|
1520
|
+
block: targetName,
|
|
1521
|
+
message: `Secret marker {{secret.${name}}} appears in a '${op.kind}' op position that is not a secret sink. A secret may only be injected into a shell(...) op or a '$ connector.tool' dispatch — never where its value could be read, bound, or emitted.`,
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
if (ctx.parsed.outputTemplate !== null) {
|
|
1527
|
+
for (const name of extractSecretRefs(ctx.parsed.outputTemplate)) {
|
|
1528
|
+
findings.push({
|
|
1529
|
+
rule: "secret-use-only",
|
|
1530
|
+
severity: "error",
|
|
1531
|
+
message: `Secret marker {{secret.${name}}} appears in the body-text output template. A secret value must never reach an output surface; reference it only inside a shell(...) or '$ connector.tool' sink op.`,
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
// Source-level backstop (adopter finding 24ce83f8). The scans above walk the
|
|
1536
|
+
// parsed op AST + template, so they miss a marker the parser DROPPED — e.g. a
|
|
1537
|
+
// malformed `emit {{secret.X}}` (no parens) parses to no op, yet the marker
|
|
1538
|
+
// sits in source. Compare every marker in the raw source against the markers
|
|
1539
|
+
// the AST scan accounted for (sink positions + non-sink positions + template);
|
|
1540
|
+
// any surplus lives in a dropped/unrecognized position and is still a
|
|
1541
|
+
// use-only violation. Counts (not names) so repeats can't mask a leak.
|
|
1542
|
+
let accounted = 0;
|
|
1543
|
+
for (const [, target] of ctx.parsed.targets) {
|
|
1544
|
+
walkOps(target.ops, (op) => {
|
|
1545
|
+
accounted += countSecretMarkers(secretSinkText(op)) + countSecretMarkers(secretNonSinkText(op));
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
if (ctx.parsed.outputTemplate !== null)
|
|
1549
|
+
accounted += countSecretMarkers(ctx.parsed.outputTemplate);
|
|
1550
|
+
const surplus = countSecretMarkers(ctx.source) - accounted;
|
|
1551
|
+
if (surplus > 0) {
|
|
1552
|
+
findings.push({
|
|
1553
|
+
rule: "secret-use-only",
|
|
1554
|
+
severity: "error",
|
|
1555
|
+
message: `A {{secret.…}} marker appears outside any recognized op (e.g. a malformed \`emit {{secret.X}}\` without parentheses, or a dropped line). A secret may only be used inside a shell(...) op or a '$ connector.tool' dispatch.`,
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return findings;
|
|
1559
|
+
},
|
|
1560
|
+
};
|
|
1561
|
+
const SECRET_UNDECLARED = {
|
|
1562
|
+
id: "secret-undeclared",
|
|
1563
|
+
severity: "error",
|
|
1564
|
+
description: "A `{{secret.NAME}}` marker is used without a matching `# Requires: secret.NAME` declaration. Secrets are declared before use so an approver sees which credentials the skill reaches.",
|
|
1565
|
+
remediation: "Add `# Requires: secret.NAME` to the frontmatter for every secret the skill references.",
|
|
1566
|
+
check: (ctx) => {
|
|
1567
|
+
const declared = new Set(ctx.parsed.secretRequires);
|
|
1568
|
+
const findings = [];
|
|
1569
|
+
const seen = new Set();
|
|
1570
|
+
const checkText = (text, block) => {
|
|
1571
|
+
for (const name of extractSecretRefs(text)) {
|
|
1572
|
+
if (declared.has(name) || seen.has(name))
|
|
1573
|
+
continue;
|
|
1574
|
+
seen.add(name);
|
|
1575
|
+
findings.push({
|
|
1576
|
+
rule: "secret-undeclared",
|
|
1577
|
+
severity: "error",
|
|
1578
|
+
message: `Secret marker {{secret.${name}}} is used but not declared. Add \`# Requires: secret.${name}\` to the frontmatter so an approver can see which secrets this skill reaches.`,
|
|
1579
|
+
...(block !== undefined ? { block } : {}),
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1584
|
+
walkOps(target.ops, (op) => {
|
|
1585
|
+
checkText(secretSinkText(op), targetName);
|
|
1586
|
+
checkText(secretNonSinkText(op), targetName);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
if (ctx.parsed.outputTemplate !== null)
|
|
1590
|
+
checkText(ctx.parsed.outputTemplate);
|
|
1591
|
+
return findings;
|
|
1592
|
+
},
|
|
1593
|
+
};
|
|
1594
|
+
const SECRET_DYNAMIC_NAME = {
|
|
1595
|
+
id: "secret-dynamic-name",
|
|
1596
|
+
severity: "error",
|
|
1597
|
+
description: "A `{{secret.…}}` marker has a non-literal name (e.g. `{{secret.${VAR}}}`) or is otherwise malformed. The secret name must be a compile-time literal so the declare-before-spend gate and the approver-visible `# Requires` reach cannot be evaded by building the name at runtime.",
|
|
1598
|
+
remediation: "Use a literal secret name: `{{secret.NAME}}` where NAME matches a `# Requires: secret.NAME` declaration. Do not interpolate (`{{secret.${VAR}}}`) — the runtime refuses dynamically-named secrets.",
|
|
1599
|
+
check: (ctx) => {
|
|
1600
|
+
const findings = [];
|
|
1601
|
+
const seen = new Set();
|
|
1602
|
+
const scan = (text, block) => {
|
|
1603
|
+
for (const bad of findMalformedSecretMarkers(text)) {
|
|
1604
|
+
if (seen.has(bad))
|
|
1605
|
+
continue;
|
|
1606
|
+
seen.add(bad);
|
|
1607
|
+
findings.push({
|
|
1608
|
+
rule: "secret-dynamic-name",
|
|
1609
|
+
severity: "error",
|
|
1610
|
+
message: `Malformed/dynamic secret marker '${bad}' — the secret name must be a compile-time literal (\`{{secret.NAME}}\`), not interpolated or built at runtime.`,
|
|
1611
|
+
...(block !== undefined ? { block } : {}),
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
for (const [targetName, target] of ctx.parsed.targets) {
|
|
1616
|
+
walkOps(target.ops, (op) => {
|
|
1617
|
+
scan(secretSinkText(op), targetName);
|
|
1618
|
+
scan(secretNonSinkText(op), targetName);
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
if (ctx.parsed.outputTemplate !== null)
|
|
1622
|
+
scan(ctx.parsed.outputTemplate);
|
|
1623
|
+
return findings;
|
|
1624
|
+
},
|
|
1625
|
+
};
|
|
1461
1626
|
const STATUS_DISABLED = {
|
|
1462
1627
|
id: "status-disabled",
|
|
1463
1628
|
severity: "error",
|
|
@@ -3410,6 +3575,9 @@ const RULES = [
|
|
|
3410
3575
|
APPEND_TO_NON_LIST,
|
|
3411
3576
|
DISABLED_SKILL_REFERENCE,
|
|
3412
3577
|
CREDENTIAL_IN_ARGS,
|
|
3578
|
+
SECRET_USE_ONLY,
|
|
3579
|
+
SECRET_UNDECLARED,
|
|
3580
|
+
SECRET_DYNAMIC_NAME,
|
|
3413
3581
|
STATUS_DISABLED,
|
|
3414
3582
|
CIRCULAR_DEPENDENCY,
|
|
3415
3583
|
MISSING_DEPENDENCY,
|