vet-skills 0.5.1 → 0.6.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/dist/index.js +493 -86
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -111,7 +111,7 @@ var require_visit = __commonJS({
|
|
|
111
111
|
"use strict";
|
|
112
112
|
var identity = require_identity();
|
|
113
113
|
var BREAK = /* @__PURE__ */ Symbol("break visit");
|
|
114
|
-
var
|
|
114
|
+
var SKIP2 = /* @__PURE__ */ Symbol("skip children");
|
|
115
115
|
var REMOVE = /* @__PURE__ */ Symbol("remove node");
|
|
116
116
|
function visit(node, visitor) {
|
|
117
117
|
const visitor_ = initVisitor(visitor);
|
|
@@ -123,7 +123,7 @@ var require_visit = __commonJS({
|
|
|
123
123
|
visit_(null, node, visitor_, Object.freeze([]));
|
|
124
124
|
}
|
|
125
125
|
visit.BREAK = BREAK;
|
|
126
|
-
visit.SKIP =
|
|
126
|
+
visit.SKIP = SKIP2;
|
|
127
127
|
visit.REMOVE = REMOVE;
|
|
128
128
|
function visit_(key, node, visitor, path) {
|
|
129
129
|
const ctrl = callVisitor(key, node, visitor, path);
|
|
@@ -171,7 +171,7 @@ var require_visit = __commonJS({
|
|
|
171
171
|
await visitAsync_(null, node, visitor_, Object.freeze([]));
|
|
172
172
|
}
|
|
173
173
|
visitAsync.BREAK = BREAK;
|
|
174
|
-
visitAsync.SKIP =
|
|
174
|
+
visitAsync.SKIP = SKIP2;
|
|
175
175
|
visitAsync.REMOVE = REMOVE;
|
|
176
176
|
async function visitAsync_(key, node, visitor, path) {
|
|
177
177
|
const ctrl = await callVisitor(key, node, visitor, path);
|
|
@@ -4011,10 +4011,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
4011
4011
|
let offset = bm.offset;
|
|
4012
4012
|
let commentEnd = null;
|
|
4013
4013
|
for (const collItem of bm.items) {
|
|
4014
|
-
const { start, key, sep, value } = collItem;
|
|
4014
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
4015
4015
|
const keyProps = resolveProps.resolveProps(start, {
|
|
4016
4016
|
indicator: "explicit-key-ind",
|
|
4017
|
-
next: key ??
|
|
4017
|
+
next: key ?? sep2?.[0],
|
|
4018
4018
|
offset,
|
|
4019
4019
|
onError,
|
|
4020
4020
|
parentIndent: bm.indent,
|
|
@@ -4028,7 +4028,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4028
4028
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
4029
4029
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
4030
4030
|
}
|
|
4031
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
4031
|
+
if (!keyProps.anchor && !keyProps.tag && !sep2) {
|
|
4032
4032
|
commentEnd = keyProps.end;
|
|
4033
4033
|
if (keyProps.comment) {
|
|
4034
4034
|
if (map.comment)
|
|
@@ -4052,7 +4052,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4052
4052
|
ctx.atKey = false;
|
|
4053
4053
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
4054
4054
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
4055
|
-
const valueProps = resolveProps.resolveProps(
|
|
4055
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
4056
4056
|
indicator: "map-value-ind",
|
|
4057
4057
|
next: value,
|
|
4058
4058
|
offset: keyNode.range[2],
|
|
@@ -4068,7 +4068,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4068
4068
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
4069
4069
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
4070
4070
|
}
|
|
4071
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
4071
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep2, null, valueProps, onError);
|
|
4072
4072
|
if (ctx.schema.compat)
|
|
4073
4073
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
4074
4074
|
offset = valueNode.range[2];
|
|
@@ -4159,7 +4159,7 @@ var require_resolve_end = __commonJS({
|
|
|
4159
4159
|
let comment = "";
|
|
4160
4160
|
if (end) {
|
|
4161
4161
|
let hasSpace = false;
|
|
4162
|
-
let
|
|
4162
|
+
let sep2 = "";
|
|
4163
4163
|
for (const token of end) {
|
|
4164
4164
|
const { source, type } = token;
|
|
4165
4165
|
switch (type) {
|
|
@@ -4173,13 +4173,13 @@ var require_resolve_end = __commonJS({
|
|
|
4173
4173
|
if (!comment)
|
|
4174
4174
|
comment = cb;
|
|
4175
4175
|
else
|
|
4176
|
-
comment +=
|
|
4177
|
-
|
|
4176
|
+
comment += sep2 + cb;
|
|
4177
|
+
sep2 = "";
|
|
4178
4178
|
break;
|
|
4179
4179
|
}
|
|
4180
4180
|
case "newline":
|
|
4181
4181
|
if (comment)
|
|
4182
|
-
|
|
4182
|
+
sep2 += source;
|
|
4183
4183
|
hasSpace = true;
|
|
4184
4184
|
break;
|
|
4185
4185
|
default:
|
|
@@ -4222,18 +4222,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4222
4222
|
let offset = fc.offset + fc.start.source.length;
|
|
4223
4223
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
4224
4224
|
const collItem = fc.items[i];
|
|
4225
|
-
const { start, key, sep, value } = collItem;
|
|
4225
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
4226
4226
|
const props = resolveProps.resolveProps(start, {
|
|
4227
4227
|
flow: fcName,
|
|
4228
4228
|
indicator: "explicit-key-ind",
|
|
4229
|
-
next: key ??
|
|
4229
|
+
next: key ?? sep2?.[0],
|
|
4230
4230
|
offset,
|
|
4231
4231
|
onError,
|
|
4232
4232
|
parentIndent: fc.indent,
|
|
4233
4233
|
startOnNewline: false
|
|
4234
4234
|
});
|
|
4235
4235
|
if (!props.found) {
|
|
4236
|
-
if (!props.anchor && !props.tag && !
|
|
4236
|
+
if (!props.anchor && !props.tag && !sep2 && !value) {
|
|
4237
4237
|
if (i === 0 && props.comma)
|
|
4238
4238
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
4239
4239
|
else if (i < fc.items.length - 1)
|
|
@@ -4287,8 +4287,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4287
4287
|
}
|
|
4288
4288
|
}
|
|
4289
4289
|
}
|
|
4290
|
-
if (!isMap && !
|
|
4291
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
4290
|
+
if (!isMap && !sep2 && !props.found) {
|
|
4291
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
|
|
4292
4292
|
coll.items.push(valueNode);
|
|
4293
4293
|
offset = valueNode.range[2];
|
|
4294
4294
|
if (isBlock(value))
|
|
@@ -4300,7 +4300,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4300
4300
|
if (isBlock(key))
|
|
4301
4301
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
4302
4302
|
ctx.atKey = false;
|
|
4303
|
-
const valueProps = resolveProps.resolveProps(
|
|
4303
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
4304
4304
|
flow: fcName,
|
|
4305
4305
|
indicator: "map-value-ind",
|
|
4306
4306
|
next: value,
|
|
@@ -4311,8 +4311,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4311
4311
|
});
|
|
4312
4312
|
if (valueProps.found) {
|
|
4313
4313
|
if (!isMap && !props.found && ctx.options.strict) {
|
|
4314
|
-
if (
|
|
4315
|
-
for (const st of
|
|
4314
|
+
if (sep2)
|
|
4315
|
+
for (const st of sep2) {
|
|
4316
4316
|
if (st === valueProps.found)
|
|
4317
4317
|
break;
|
|
4318
4318
|
if (st.type === "newline") {
|
|
@@ -4329,7 +4329,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4329
4329
|
else
|
|
4330
4330
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
4331
4331
|
}
|
|
4332
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
4332
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep2, null, valueProps, onError) : null;
|
|
4333
4333
|
if (valueNode) {
|
|
4334
4334
|
if (isBlock(value))
|
|
4335
4335
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -4509,7 +4509,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4509
4509
|
chompStart = i + 1;
|
|
4510
4510
|
}
|
|
4511
4511
|
let value = "";
|
|
4512
|
-
let
|
|
4512
|
+
let sep2 = "";
|
|
4513
4513
|
let prevMoreIndented = false;
|
|
4514
4514
|
for (let i = 0; i < contentStart; ++i)
|
|
4515
4515
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -4526,24 +4526,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4526
4526
|
indent = "";
|
|
4527
4527
|
}
|
|
4528
4528
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
4529
|
-
value +=
|
|
4530
|
-
|
|
4529
|
+
value += sep2 + indent.slice(trimIndent) + content;
|
|
4530
|
+
sep2 = "\n";
|
|
4531
4531
|
} else if (indent.length > trimIndent || content[0] === " ") {
|
|
4532
|
-
if (
|
|
4533
|
-
|
|
4534
|
-
else if (!prevMoreIndented &&
|
|
4535
|
-
|
|
4536
|
-
value +=
|
|
4537
|
-
|
|
4532
|
+
if (sep2 === " ")
|
|
4533
|
+
sep2 = "\n";
|
|
4534
|
+
else if (!prevMoreIndented && sep2 === "\n")
|
|
4535
|
+
sep2 = "\n\n";
|
|
4536
|
+
value += sep2 + indent.slice(trimIndent) + content;
|
|
4537
|
+
sep2 = "\n";
|
|
4538
4538
|
prevMoreIndented = true;
|
|
4539
4539
|
} else if (content === "") {
|
|
4540
|
-
if (
|
|
4540
|
+
if (sep2 === "\n")
|
|
4541
4541
|
value += "\n";
|
|
4542
4542
|
else
|
|
4543
|
-
|
|
4543
|
+
sep2 = "\n";
|
|
4544
4544
|
} else {
|
|
4545
|
-
value +=
|
|
4546
|
-
|
|
4545
|
+
value += sep2 + content;
|
|
4546
|
+
sep2 = " ";
|
|
4547
4547
|
prevMoreIndented = false;
|
|
4548
4548
|
}
|
|
4549
4549
|
}
|
|
@@ -4726,25 +4726,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
4726
4726
|
trimBoth = /^[ \t]+|[ \t]+$/g;
|
|
4727
4727
|
}
|
|
4728
4728
|
let res = match[1].replace(trimEnd, "");
|
|
4729
|
-
let
|
|
4729
|
+
let sep2 = " ";
|
|
4730
4730
|
let pos = line.lastIndex;
|
|
4731
4731
|
while (match = line.exec(source)) {
|
|
4732
4732
|
const lm = match[1].replace(trimBoth, "");
|
|
4733
4733
|
if (lm === "") {
|
|
4734
|
-
if (
|
|
4735
|
-
res +=
|
|
4734
|
+
if (sep2 === "\n")
|
|
4735
|
+
res += sep2;
|
|
4736
4736
|
else
|
|
4737
|
-
|
|
4737
|
+
sep2 = "\n";
|
|
4738
4738
|
} else {
|
|
4739
|
-
res +=
|
|
4740
|
-
|
|
4739
|
+
res += sep2 + lm;
|
|
4740
|
+
sep2 = " ";
|
|
4741
4741
|
}
|
|
4742
4742
|
pos = line.lastIndex;
|
|
4743
4743
|
}
|
|
4744
4744
|
const last = /[ \t]*(.*)/sy;
|
|
4745
4745
|
last.lastIndex = pos;
|
|
4746
4746
|
match = last.exec(source);
|
|
4747
|
-
return res +
|
|
4747
|
+
return res + sep2 + (match?.[1] ?? "");
|
|
4748
4748
|
}
|
|
4749
4749
|
function doubleQuotedValue(source, onError) {
|
|
4750
4750
|
let res = "";
|
|
@@ -5554,14 +5554,14 @@ var require_cst_stringify = __commonJS({
|
|
|
5554
5554
|
}
|
|
5555
5555
|
}
|
|
5556
5556
|
}
|
|
5557
|
-
function stringifyItem({ start, key, sep, value }) {
|
|
5557
|
+
function stringifyItem({ start, key, sep: sep2, value }) {
|
|
5558
5558
|
let res = "";
|
|
5559
5559
|
for (const st of start)
|
|
5560
5560
|
res += st.source;
|
|
5561
5561
|
if (key)
|
|
5562
5562
|
res += stringifyToken(key);
|
|
5563
|
-
if (
|
|
5564
|
-
for (const st of
|
|
5563
|
+
if (sep2)
|
|
5564
|
+
for (const st of sep2)
|
|
5565
5565
|
res += st.source;
|
|
5566
5566
|
if (value)
|
|
5567
5567
|
res += stringifyToken(value);
|
|
@@ -5576,7 +5576,7 @@ var require_cst_visit = __commonJS({
|
|
|
5576
5576
|
"../../node_modules/.pnpm/yaml@2.9.1/node_modules/yaml/dist/parse/cst-visit.js"(exports) {
|
|
5577
5577
|
"use strict";
|
|
5578
5578
|
var BREAK = /* @__PURE__ */ Symbol("break visit");
|
|
5579
|
-
var
|
|
5579
|
+
var SKIP2 = /* @__PURE__ */ Symbol("skip children");
|
|
5580
5580
|
var REMOVE = /* @__PURE__ */ Symbol("remove item");
|
|
5581
5581
|
function visit(cst, visitor) {
|
|
5582
5582
|
if ("type" in cst && cst.type === "document")
|
|
@@ -5584,7 +5584,7 @@ var require_cst_visit = __commonJS({
|
|
|
5584
5584
|
_visit(Object.freeze([]), cst, visitor);
|
|
5585
5585
|
}
|
|
5586
5586
|
visit.BREAK = BREAK;
|
|
5587
|
-
visit.SKIP =
|
|
5587
|
+
visit.SKIP = SKIP2;
|
|
5588
5588
|
visit.REMOVE = REMOVE;
|
|
5589
5589
|
visit.itemAtPath = (cst, path) => {
|
|
5590
5590
|
let item = cst;
|
|
@@ -6728,18 +6728,18 @@ var require_parser = __commonJS({
|
|
|
6728
6728
|
if (this.type === "map-value-ind") {
|
|
6729
6729
|
const prev = getPrevProps(this.peek(2));
|
|
6730
6730
|
const start = getFirstKeyStartProps(prev);
|
|
6731
|
-
let
|
|
6731
|
+
let sep2;
|
|
6732
6732
|
if (scalar.end) {
|
|
6733
|
-
|
|
6734
|
-
|
|
6733
|
+
sep2 = scalar.end;
|
|
6734
|
+
sep2.push(this.sourceToken);
|
|
6735
6735
|
delete scalar.end;
|
|
6736
6736
|
} else
|
|
6737
|
-
|
|
6737
|
+
sep2 = [this.sourceToken];
|
|
6738
6738
|
const map = {
|
|
6739
6739
|
type: "block-map",
|
|
6740
6740
|
offset: scalar.offset,
|
|
6741
6741
|
indent: scalar.indent,
|
|
6742
|
-
items: [{ start, key: scalar, sep }]
|
|
6742
|
+
items: [{ start, key: scalar, sep: sep2 }]
|
|
6743
6743
|
};
|
|
6744
6744
|
this.onKeyLine = true;
|
|
6745
6745
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -6892,15 +6892,15 @@ var require_parser = __commonJS({
|
|
|
6892
6892
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
6893
6893
|
const start2 = getFirstKeyStartProps(it.start);
|
|
6894
6894
|
const key = it.key;
|
|
6895
|
-
const
|
|
6896
|
-
|
|
6895
|
+
const sep2 = it.sep;
|
|
6896
|
+
sep2.push(this.sourceToken);
|
|
6897
6897
|
delete it.key;
|
|
6898
6898
|
delete it.sep;
|
|
6899
6899
|
this.stack.push({
|
|
6900
6900
|
type: "block-map",
|
|
6901
6901
|
offset: this.offset,
|
|
6902
6902
|
indent: this.indent,
|
|
6903
|
-
items: [{ start: start2, key, sep }]
|
|
6903
|
+
items: [{ start: start2, key, sep: sep2 }]
|
|
6904
6904
|
});
|
|
6905
6905
|
} else if (start.length > 0) {
|
|
6906
6906
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -7094,13 +7094,13 @@ var require_parser = __commonJS({
|
|
|
7094
7094
|
const prev = getPrevProps(parent);
|
|
7095
7095
|
const start = getFirstKeyStartProps(prev);
|
|
7096
7096
|
fixFlowSeqItems(fc);
|
|
7097
|
-
const
|
|
7098
|
-
|
|
7097
|
+
const sep2 = fc.end.splice(1, fc.end.length);
|
|
7098
|
+
sep2.push(this.sourceToken);
|
|
7099
7099
|
const map = {
|
|
7100
7100
|
type: "block-map",
|
|
7101
7101
|
offset: fc.offset,
|
|
7102
7102
|
indent: fc.indent,
|
|
7103
|
-
items: [{ start, key: fc, sep }]
|
|
7103
|
+
items: [{ start, key: fc, sep: sep2 }]
|
|
7104
7104
|
};
|
|
7105
7105
|
this.onKeyLine = true;
|
|
7106
7106
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -7614,10 +7614,10 @@ var init_dist = __esm({
|
|
|
7614
7614
|
init_dist();
|
|
7615
7615
|
import { parseArgs } from "node:util";
|
|
7616
7616
|
import { spawnSync } from "node:child_process";
|
|
7617
|
-
import { readFileSync as
|
|
7618
|
-
import { resolve, join as
|
|
7617
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "node:fs";
|
|
7618
|
+
import { resolve as resolve2, join as join6, dirname, basename } from "node:path";
|
|
7619
7619
|
import { hostname, platform as platform2, userInfo } from "node:os";
|
|
7620
|
-
import { createHash } from "node:crypto";
|
|
7620
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7621
7621
|
import { createInterface } from "node:readline";
|
|
7622
7622
|
|
|
7623
7623
|
// src/check.ts
|
|
@@ -7629,7 +7629,7 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync, appendFileSync, chm
|
|
|
7629
7629
|
import { homedir } from "node:os";
|
|
7630
7630
|
import { join } from "node:path";
|
|
7631
7631
|
import { randomUUID } from "node:crypto";
|
|
7632
|
-
var VERSION = "0.
|
|
7632
|
+
var VERSION = "0.6.0";
|
|
7633
7633
|
var HOME = process.env.VET_SKILLS_HOME ?? join(homedir(), ".vet-skills");
|
|
7634
7634
|
var API = (process.env.KILLERSKILLS_API ?? "https://killerskills.ai").replace(/\/$/, "");
|
|
7635
7635
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -7805,6 +7805,11 @@ function report(body) {
|
|
|
7805
7805
|
if (!org) return Promise.resolve({ ok: false, error: "not joined", offline: false });
|
|
7806
7806
|
return call(`/api/v1/orgs/${org.slug}/agents/${org.agentId}/report`, { method: "POST", body: JSON.stringify(body), headers: orgHeaders() }, 8e3);
|
|
7807
7807
|
}
|
|
7808
|
+
function runtimeReport(body) {
|
|
7809
|
+
const org = readOrg();
|
|
7810
|
+
if (!org) return Promise.resolve({ ok: false, error: "not joined", offline: false });
|
|
7811
|
+
return call(`/api/v1/orgs/${org.slug}/agents/${org.agentId}/runtime`, { method: "POST", body: JSON.stringify(body), headers: orgHeaders() }, 6e3);
|
|
7812
|
+
}
|
|
7808
7813
|
function confirmRemoval(removalId, body) {
|
|
7809
7814
|
const org = readOrg();
|
|
7810
7815
|
if (!org) return Promise.resolve({ ok: false, error: "not joined", offline: false });
|
|
@@ -7864,6 +7869,151 @@ function fmtInstalls(n) {
|
|
|
7864
7869
|
return n.toLocaleString("en-US");
|
|
7865
7870
|
}
|
|
7866
7871
|
|
|
7872
|
+
// src/attest.ts
|
|
7873
|
+
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
7874
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7875
|
+
import { homedir as homedir2 } from "node:os";
|
|
7876
|
+
import { join as join2, relative, resolve, sep } from "node:path";
|
|
7877
|
+
var LOCKFILE = "vet-skills.lock";
|
|
7878
|
+
var PAYLOAD_TYPE = "application/vnd.killerskills.verdict+json";
|
|
7879
|
+
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".pglite", ".next", "__pycache__", ".venv"]);
|
|
7880
|
+
function bundleHash(root) {
|
|
7881
|
+
const h = createHash("sha256");
|
|
7882
|
+
const walk = (dir) => {
|
|
7883
|
+
for (const name of readdirSync(dir).sort()) {
|
|
7884
|
+
if (SKIP.has(name)) continue;
|
|
7885
|
+
const p = join2(dir, name);
|
|
7886
|
+
const st = statSync2(p);
|
|
7887
|
+
if (st.isDirectory()) walk(p);
|
|
7888
|
+
else if (st.isFile() && st.size <= 2 * 1024 * 1024) {
|
|
7889
|
+
h.update(relative(root, p).split(sep).join("/"));
|
|
7890
|
+
h.update("\0");
|
|
7891
|
+
h.update(readFileSync2(p));
|
|
7892
|
+
h.update("\0");
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
};
|
|
7896
|
+
walk(root);
|
|
7897
|
+
return h.digest("hex");
|
|
7898
|
+
}
|
|
7899
|
+
function pae(payloadType, payload) {
|
|
7900
|
+
const t = Buffer.from(payloadType);
|
|
7901
|
+
return Buffer.concat([Buffer.from(`DSSEv1 ${t.length} `), t, Buffer.from(` ${payload.length} `), payload]);
|
|
7902
|
+
}
|
|
7903
|
+
function verifyEnvelope(env, keys) {
|
|
7904
|
+
if (env.payloadType !== PAYLOAD_TYPE) return { ok: false, statement: null, kid: null, reason: `unexpected payload type ${env.payloadType}` };
|
|
7905
|
+
const payload = Buffer.from(env.payload, "base64");
|
|
7906
|
+
const trusted = (process.env.VET_SKILLS_TRUSTED_KEYS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7907
|
+
for (const sig of env.signatures) {
|
|
7908
|
+
const k = keys.find((x) => x.kid === sig.keyid);
|
|
7909
|
+
if (!k) continue;
|
|
7910
|
+
if (trusted.length && !trusted.includes(k.kid)) return { ok: false, statement: null, kid: k.kid, reason: `key ${k.kid} is not in VET_SKILLS_TRUSTED_KEYS` };
|
|
7911
|
+
if (k.dev) return { ok: false, statement: null, kid: k.kid, reason: "signed with a development key" };
|
|
7912
|
+
try {
|
|
7913
|
+
const pub = createPublicKey({ key: { kty: k.kty, crv: k.crv, x: k.x }, format: "jwk" });
|
|
7914
|
+
if (cryptoVerify(null, pae(env.payloadType, payload), pub, Buffer.from(sig.sig, "base64"))) return { ok: true, statement: JSON.parse(payload.toString("utf8")), kid: k.kid, reason: "signature valid" };
|
|
7915
|
+
return { ok: false, statement: null, kid: k.kid, reason: "signature does not verify" };
|
|
7916
|
+
} catch (e) {
|
|
7917
|
+
return { ok: false, statement: null, kid: k.kid, reason: e.message };
|
|
7918
|
+
}
|
|
7919
|
+
}
|
|
7920
|
+
return { ok: false, statement: null, kid: env.signatures[0]?.keyid ?? null, reason: "no known key for this signature" };
|
|
7921
|
+
}
|
|
7922
|
+
async function getJson(url) {
|
|
7923
|
+
try {
|
|
7924
|
+
const r = await fetch(url, { headers: { accept: "application/json", "user-agent": `vet-skills/${VERSION}` }, signal: AbortSignal.timeout(8e3) });
|
|
7925
|
+
return r.ok ? await r.json() : null;
|
|
7926
|
+
} catch {
|
|
7927
|
+
return null;
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
async function fetchKeys() {
|
|
7931
|
+
const r = await getJson(`${API}/.well-known/killerskills-keys.json`);
|
|
7932
|
+
return r?.keys ?? [];
|
|
7933
|
+
}
|
|
7934
|
+
async function fetchAttestationByHash(hash) {
|
|
7935
|
+
return getJson(`${API}/api/v1/verdicts/${hash}/attestation`);
|
|
7936
|
+
}
|
|
7937
|
+
function installedSkills() {
|
|
7938
|
+
const out = [];
|
|
7939
|
+
for (const base of [join2(process.cwd(), ".claude", "skills"), join2(homedir2(), ".claude", "skills"), join2(homedir2(), ".claude", "plugins")]) {
|
|
7940
|
+
if (!existsSync2(base)) continue;
|
|
7941
|
+
for (const name of readdirSync(base).sort()) {
|
|
7942
|
+
const p = join2(base, name);
|
|
7943
|
+
if (statSync2(p).isDirectory() && !out.some((x) => x.name === name)) out.push({ name, path: p });
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
return out;
|
|
7947
|
+
}
|
|
7948
|
+
async function verifyOne(target, keys) {
|
|
7949
|
+
const dir = resolve(target);
|
|
7950
|
+
let hash;
|
|
7951
|
+
let subject = target;
|
|
7952
|
+
let page = null;
|
|
7953
|
+
if (existsSync2(dir) && statSync2(dir).isDirectory()) {
|
|
7954
|
+
hash = bundleHash(dir);
|
|
7955
|
+
subject = `${target} (${hash.slice(0, 12)}\u2026)`;
|
|
7956
|
+
} else {
|
|
7957
|
+
const [name, ver] = target.split("@").length > 2 ? [target.slice(0, target.lastIndexOf("@")), target.slice(target.lastIndexOf("@") + 1)] : target.includes("@") && !target.startsWith("@") ? target.split("@") : [target, void 0];
|
|
7958
|
+
const r = await resolveSkill(name);
|
|
7959
|
+
if (!r.ok) return { subject, hash: "", ok: false, detail: r.offline ? "offline" : `not found: ${r.error}`, statement: null };
|
|
7960
|
+
if (ver && r.data.version.semver !== ver) return { subject, hash: "", ok: false, detail: `latest scored version is ${r.data.version.semver}; verify a local checkout of ${ver} with a path instead`, statement: null };
|
|
7961
|
+
hash = r.data.version.artifact_hash;
|
|
7962
|
+
page = r.data.skill.page;
|
|
7963
|
+
subject = `${r.data.skill.name}@${r.data.version.semver}`;
|
|
7964
|
+
const local = installedSkills().find((s) => s.name === r.data.skill.name);
|
|
7965
|
+
if (local) {
|
|
7966
|
+
const lh = bundleHash(local.path);
|
|
7967
|
+
if (lh !== hash) return { subject, hash: lh, ok: false, detail: `installed copy at ${local.path} hashes to ${lh.slice(0, 12)}\u2026, attested version is ${hash.slice(0, 12)}\u2026`, statement: null };
|
|
7968
|
+
}
|
|
7969
|
+
}
|
|
7970
|
+
const env = await fetchAttestationByHash(hash);
|
|
7971
|
+
if (!env) return { subject, hash, ok: false, detail: `no attestation for ${hash.slice(0, 12)}\u2026 (unscored, demo or signing not configured)${page ? ` \xB7 ${API}${page}` : ""}`, statement: null };
|
|
7972
|
+
const v = verifyEnvelope(env, keys);
|
|
7973
|
+
if (!v.ok || !v.statement) return { subject, hash, ok: false, detail: v.reason, statement: null };
|
|
7974
|
+
if (v.statement.subject.artifact_hash !== hash) return { subject, hash, ok: false, detail: "attestation is for a different artifact", statement: v.statement };
|
|
7975
|
+
return { subject, hash, ok: true, detail: `${v.statement.predicate.label} ${v.statement.predicate.score} \xB7 engine ${v.statement.predicate.engine_version} \xB7 signed by ${v.kid}${v.statement.predicate.superseded ? " \xB7 superseded" : ""}`, statement: v.statement };
|
|
7976
|
+
}
|
|
7977
|
+
async function writeLock(dir = process.cwd()) {
|
|
7978
|
+
const keys = await fetchKeys();
|
|
7979
|
+
const skills = [];
|
|
7980
|
+
const missing = [];
|
|
7981
|
+
for (const s of installedSkills()) {
|
|
7982
|
+
const hash = bundleHash(s.path);
|
|
7983
|
+
const env = await fetchAttestationByHash(hash);
|
|
7984
|
+
const v = env ? verifyEnvelope(env, keys) : null;
|
|
7985
|
+
if (!v?.ok || !v.statement) missing.push(s.name);
|
|
7986
|
+
skills.push({ name: s.name, path: s.path, artifact_hash: hash, verdict: v?.statement ? { id: v.statement.predicate.verdict_id, label: v.statement.predicate.label, score: v.statement.predicate.score, engine_version: v.statement.predicate.engine_version } : null, attestation: v?.ok ? env : null });
|
|
7987
|
+
}
|
|
7988
|
+
const lock = { version: 1, generated_at: (/* @__PURE__ */ new Date()).toISOString(), generated_by: `vet-skills ${VERSION}`, issuer: API, keys: keys.filter((k) => !k.dev), skills };
|
|
7989
|
+
const file = join2(dir, LOCKFILE);
|
|
7990
|
+
writeFileSync2(file, `${JSON.stringify(lock, null, 2)}
|
|
7991
|
+
`);
|
|
7992
|
+
return { file, lock, missing };
|
|
7993
|
+
}
|
|
7994
|
+
function readLock(dir = process.cwd()) {
|
|
7995
|
+
const file = join2(dir, LOCKFILE);
|
|
7996
|
+
if (!existsSync2(file)) return null;
|
|
7997
|
+
try {
|
|
7998
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
7999
|
+
} catch {
|
|
8000
|
+
return null;
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
function verifyLock(lock) {
|
|
8004
|
+
return lock.skills.map((e) => {
|
|
8005
|
+
const path = existsSync2(e.path) ? e.path : installedSkills().find((s) => s.name === e.name)?.path;
|
|
8006
|
+
if (!path) return { subject: e.name, hash: e.artifact_hash, ok: false, detail: "not installed", statement: null };
|
|
8007
|
+
const hash = bundleHash(path);
|
|
8008
|
+
if (hash !== e.artifact_hash) return { subject: e.name, hash, ok: false, detail: `changed on disk: ${hash.slice(0, 12)}\u2026 (locked ${e.artifact_hash.slice(0, 12)}\u2026)`, statement: null };
|
|
8009
|
+
if (!e.attestation) return { subject: e.name, hash, ok: true, detail: "hash matches \xB7 pinned by hash only (no attestation when locked)", statement: null };
|
|
8010
|
+
const v = verifyEnvelope(e.attestation, lock.keys);
|
|
8011
|
+
if (!v.ok || !v.statement) return { subject: e.name, hash, ok: false, detail: v.reason, statement: null };
|
|
8012
|
+
if (v.statement.subject.artifact_hash !== hash) return { subject: e.name, hash, ok: false, detail: "attestation is for a different artifact", statement: v.statement };
|
|
8013
|
+
return { subject: e.name, hash, ok: true, detail: `${v.statement.predicate.label} ${v.statement.predicate.score} \xB7 engine ${v.statement.predicate.engine_version} \xB7 signed by ${v.kid}`, statement: v.statement };
|
|
8014
|
+
});
|
|
8015
|
+
}
|
|
8016
|
+
|
|
7867
8017
|
// src/check.ts
|
|
7868
8018
|
function toVerdictInput(r) {
|
|
7869
8019
|
if (!r.verdict) return null;
|
|
@@ -7945,6 +8095,12 @@ async function check(q, opts = {}) {
|
|
|
7945
8095
|
if (telemetryNoticePending()) process.stderr.write(c.muted("vet-skills sends anonymous usage (skill name, version, decision, agent type; never code, prompts or paths). Opt out: vet-skills telemetry off") + "\n");
|
|
7946
8096
|
await sendTelemetry({ skill: resolved?.skill.name ?? q, semver: resolved?.version.semver ?? null, registry: resolved?.skill.registry ?? null, decision: decision.decision, would_be: decision.wouldBe ?? null, agent_kind: opts.agentKind ?? "other", hook_version: VERSION, os: platform(), offline }).catch(() => null);
|
|
7947
8097
|
}
|
|
8098
|
+
const lock = readLock();
|
|
8099
|
+
const pinned = lock && resolved ? lock.skills.find((e) => e.name === resolved.skill.name) : null;
|
|
8100
|
+
if (pinned && resolved && pinned.artifact_hash !== resolved.version.artifact_hash) {
|
|
8101
|
+
const why = `vet-skills.lock pins ${resolved.skill.name} to ${pinned.artifact_hash.slice(0, 12)}\u2026; registry has ${resolved.version.artifact_hash.slice(0, 12)}\u2026 (${resolved.version.semver}). Run \`vet-skills lock\` to accept it.`;
|
|
8102
|
+
return { resolved, decision: { ...decision, decision: "block", wouldBe: "block", reason: why }, source, cacheAge, remote, offline, policyLabel };
|
|
8103
|
+
}
|
|
7948
8104
|
return { resolved, decision, source, cacheAge, remote, offline, policyLabel };
|
|
7949
8105
|
}
|
|
7950
8106
|
function renderCheck(r) {
|
|
@@ -8028,17 +8184,17 @@ function restrictionText(r) {
|
|
|
8028
8184
|
}
|
|
8029
8185
|
|
|
8030
8186
|
// src/hook.ts
|
|
8031
|
-
import { existsSync as
|
|
8032
|
-
import { homedir as
|
|
8033
|
-
import { join as
|
|
8187
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8188
|
+
import { homedir as homedir4 } from "node:os";
|
|
8189
|
+
import { join as join4 } from "node:path";
|
|
8034
8190
|
|
|
8035
8191
|
// src/sync.ts
|
|
8036
|
-
import { existsSync as
|
|
8037
|
-
import { homedir as
|
|
8038
|
-
import { join as
|
|
8192
|
+
import { existsSync as existsSync3, rmSync } from "node:fs";
|
|
8193
|
+
import { homedir as homedir3 } from "node:os";
|
|
8194
|
+
import { join as join3 } from "node:path";
|
|
8039
8195
|
function skillDirs(name) {
|
|
8040
8196
|
const bare = name.replace(/^.*\//, "").replace(/@.*$/, "");
|
|
8041
|
-
return [
|
|
8197
|
+
return [join3(homedir3(), ".claude", "skills", bare), join3(process.cwd(), ".claude", "skills", bare), join3(homedir3(), ".claude", "plugins", bare)];
|
|
8042
8198
|
}
|
|
8043
8199
|
async function applyRemovals(pending, log = () => {
|
|
8044
8200
|
}) {
|
|
@@ -8049,7 +8205,7 @@ async function applyRemovals(pending, log = () => {
|
|
|
8049
8205
|
const detail = [];
|
|
8050
8206
|
let failed = false;
|
|
8051
8207
|
for (const it of items) {
|
|
8052
|
-
const dirs = skillDirs(it.skill).filter((d) =>
|
|
8208
|
+
const dirs = skillDirs(it.skill).filter((d) => existsSync3(d));
|
|
8053
8209
|
if (!dirs.length) {
|
|
8054
8210
|
detail.push(`${it.skill}: not on disk`);
|
|
8055
8211
|
continue;
|
|
@@ -8200,24 +8356,201 @@ ${out}
|
|
|
8200
8356
|
return 0;
|
|
8201
8357
|
}
|
|
8202
8358
|
function installClaudeCodeHook() {
|
|
8203
|
-
const dir =
|
|
8204
|
-
const path =
|
|
8205
|
-
if (!
|
|
8359
|
+
const dir = join4(homedir4(), ".claude");
|
|
8360
|
+
const path = join4(dir, "settings.json");
|
|
8361
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
8206
8362
|
let settings = {};
|
|
8207
|
-
if (
|
|
8363
|
+
if (existsSync4(path)) {
|
|
8208
8364
|
try {
|
|
8209
|
-
settings = JSON.parse(
|
|
8365
|
+
settings = JSON.parse(readFileSync3(path, "utf8"));
|
|
8210
8366
|
} catch {
|
|
8211
8367
|
settings = {};
|
|
8212
8368
|
}
|
|
8213
8369
|
}
|
|
8214
8370
|
const hooks = settings.hooks ??= {};
|
|
8215
8371
|
const pre = hooks.PreToolUse ??= [];
|
|
8216
|
-
const
|
|
8217
|
-
|
|
8218
|
-
pre.
|
|
8219
|
-
|
|
8220
|
-
|
|
8372
|
+
const post = hooks.PostToolUse ??= [];
|
|
8373
|
+
let changed = false;
|
|
8374
|
+
if (!pre.some((h) => h.hooks?.some((x) => /vet-skills hook eval/.test(x.command)))) {
|
|
8375
|
+
pre.push({ matcher: "Bash", hooks: [{ type: "command", command: "vet-skills hook eval" }] });
|
|
8376
|
+
changed = true;
|
|
8377
|
+
}
|
|
8378
|
+
if (!post.some((h) => h.hooks?.some((x) => /vet-skills hook observe/.test(x.command)))) {
|
|
8379
|
+
post.push({ matcher: "Bash|Read|Write|Edit|MultiEdit|NotebookEdit|WebFetch|Skill|mcp__.*", hooks: [{ type: "command", command: "vet-skills hook observe" }] });
|
|
8380
|
+
changed = true;
|
|
8381
|
+
}
|
|
8382
|
+
if (changed) writeFileSync3(path, JSON.stringify(settings, null, 2) + "\n");
|
|
8383
|
+
return { path, changed };
|
|
8384
|
+
}
|
|
8385
|
+
|
|
8386
|
+
// src/runtime.ts
|
|
8387
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8388
|
+
import { homedir as homedir5 } from "node:os";
|
|
8389
|
+
import { join as join5 } from "node:path";
|
|
8390
|
+
var ACTIVE_MS = 30 * 6e4;
|
|
8391
|
+
var CRED = /(^|\/)(\.ssh|\.aws|\.npmrc|\.netrc|\.config\/gh|\.docker\/config\.json|\.kube\/config|\.gnupg|credentials|\.env(\.|$)|id_(rsa|ed25519|ecdsa)|keychain)/i;
|
|
8392
|
+
var MEMORY = /(^|\/)(MEMORY\.md|CLAUDE\.md|AGENTS\.md|\.cursorrules|\.clinerules|\.windsurfrules|GEMINI\.md|\.github\/copilot-instructions\.md)$/i;
|
|
8393
|
+
var DIR = join5(HOME, "runtime");
|
|
8394
|
+
function sessionFile(id) {
|
|
8395
|
+
return join5(DIR, `${id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80)}.json`);
|
|
8396
|
+
}
|
|
8397
|
+
function readSession(id) {
|
|
8398
|
+
try {
|
|
8399
|
+
return JSON.parse(readFileSync4(sessionFile(id), "utf8"));
|
|
8400
|
+
} catch {
|
|
8401
|
+
return { active: null, verdicts: {}, reported: [] };
|
|
8402
|
+
}
|
|
8403
|
+
}
|
|
8404
|
+
function writeSession(id, s) {
|
|
8405
|
+
if (!existsSync5(DIR)) mkdirSync3(DIR, { recursive: true });
|
|
8406
|
+
writeFileSync4(sessionFile(id), JSON.stringify(s));
|
|
8407
|
+
}
|
|
8408
|
+
function hostsIn(cmd2) {
|
|
8409
|
+
const out = /* @__PURE__ */ new Set();
|
|
8410
|
+
for (const m of cmd2.matchAll(/https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?/g)) out.add(m[1].toLowerCase());
|
|
8411
|
+
for (const m of cmd2.matchAll(/\b(?:ssh|scp|sftp|nc|ncat|netcat|telnet)\s+(?:-\S+\s+)*(?:[\w.-]+@)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|\d{1,3}(?:\.\d{1,3}){3})/g)) out.add(m[1].toLowerCase());
|
|
8412
|
+
return [...out];
|
|
8413
|
+
}
|
|
8414
|
+
function pathsIn(cmd2) {
|
|
8415
|
+
const read = /* @__PURE__ */ new Set();
|
|
8416
|
+
const written = /* @__PURE__ */ new Set();
|
|
8417
|
+
const norm = (p) => p.replace(/^\$HOME/, "~").replace(/^["']|["']$/g, "");
|
|
8418
|
+
for (const m of cmd2.matchAll(/(?:>>?|tee\s+(?:-a\s+)?)\s*["']?((?:~|\/|\$HOME)[^\s"'|;&]+)/g)) written.add(norm(m[1]));
|
|
8419
|
+
for (const m of cmd2.matchAll(/(?:^|[\s|;&(])(?:cat|less|head|tail|grep|cp|scp|base64|xxd|od|strings|python3?|node)\s+(?:-\S+\s+)*["']?((?:~|\/|\$HOME)[^\s"'|;&]+)/g)) read.add(norm(m[1]));
|
|
8420
|
+
for (const m of cmd2.matchAll(/["']?((?:~|\$HOME)\/\.(?:ssh|aws|npmrc|netrc|config\/gh|docker|kube|gnupg)[^\s"'|;&]*)/g)) if (!written.has(norm(m[1]))) read.add(norm(m[1]));
|
|
8421
|
+
return { read: [...read], written: [...written] };
|
|
8422
|
+
}
|
|
8423
|
+
var isOutsideProject = (p) => (p.startsWith("~") || p.startsWith("/")) && !p.startsWith(process.cwd()) && !p.startsWith("/tmp") && !p.startsWith("/dev");
|
|
8424
|
+
var tilde = (p) => p.replace(homedir5(), "~");
|
|
8425
|
+
function attributed(session, toolName) {
|
|
8426
|
+
const m = toolName.match(/^mcp__([^_]+(?:_[^_]+)*?)__/);
|
|
8427
|
+
if (m) return m[1];
|
|
8428
|
+
if (session.active && Date.now() - session.active.since < ACTIVE_MS) return session.active.skill;
|
|
8429
|
+
return null;
|
|
8430
|
+
}
|
|
8431
|
+
async function verdictFor(session, skill) {
|
|
8432
|
+
const c2 = session.verdicts[skill];
|
|
8433
|
+
if (c2 && Date.now() - c2.at < 36e5) return c2.r;
|
|
8434
|
+
const r = await resolveSkill(skill).catch(() => null);
|
|
8435
|
+
session.verdicts[skill] = { at: Date.now(), r: r && r.ok ? r.data : null };
|
|
8436
|
+
return session.verdicts[skill].r;
|
|
8437
|
+
}
|
|
8438
|
+
function parseToolEvent(toolName, input) {
|
|
8439
|
+
const o = { kind: "other", hosts: [], read: [], written: [], skillInvoked: null };
|
|
8440
|
+
if (toolName === "Skill") {
|
|
8441
|
+
o.kind = "Skill";
|
|
8442
|
+
o.skillInvoked = String(input.skill ?? input.name ?? input.command ?? "").replace(/^\//, "").split(/\s/)[0] || null;
|
|
8443
|
+
return o;
|
|
8444
|
+
}
|
|
8445
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
8446
|
+
o.kind = "Bash";
|
|
8447
|
+
o.hosts = hostsIn(input.command);
|
|
8448
|
+
const p = pathsIn(input.command);
|
|
8449
|
+
o.read = p.read;
|
|
8450
|
+
o.written = p.written;
|
|
8451
|
+
return o;
|
|
8452
|
+
}
|
|
8453
|
+
if (toolName === "Read" && typeof input.file_path === "string") {
|
|
8454
|
+
o.kind = "Read";
|
|
8455
|
+
o.read = [tilde(input.file_path)];
|
|
8456
|
+
return o;
|
|
8457
|
+
}
|
|
8458
|
+
if ((toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit" || toolName === "NotebookEdit") && typeof input.file_path === "string") {
|
|
8459
|
+
o.kind = "Write";
|
|
8460
|
+
o.written = [tilde(input.file_path)];
|
|
8461
|
+
return o;
|
|
8462
|
+
}
|
|
8463
|
+
if (toolName === "WebFetch" && typeof input.url === "string") {
|
|
8464
|
+
o.kind = "WebFetch";
|
|
8465
|
+
try {
|
|
8466
|
+
o.hosts = [new URL(input.url).hostname.toLowerCase()];
|
|
8467
|
+
} catch {
|
|
8468
|
+
}
|
|
8469
|
+
return o;
|
|
8470
|
+
}
|
|
8471
|
+
if (toolName.startsWith("mcp__")) o.kind = "mcp";
|
|
8472
|
+
return o;
|
|
8473
|
+
}
|
|
8474
|
+
function deviations(skill, r, o, tool, session) {
|
|
8475
|
+
const out = [];
|
|
8476
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
8477
|
+
const semver = r?.version.semver ?? null;
|
|
8478
|
+
const page = r?.skill.page ?? null;
|
|
8479
|
+
const trace = r?.trace_summary ?? null;
|
|
8480
|
+
const declared = new Set((r?.version.declared_permissions.network ?? []).map((h) => h.toLowerCase()));
|
|
8481
|
+
const seenHosts = new Set((trace?.egress_hosts ?? []).map((h) => h.host.toLowerCase()));
|
|
8482
|
+
const seenReads = trace?.files_read ?? [];
|
|
8483
|
+
const seenWrites = trace?.files_written ?? [];
|
|
8484
|
+
const mk = (kind, detail) => out.push({ kind, skill, semver, detail, tool, at, session, page });
|
|
8485
|
+
for (const h of o.hosts) if (!declared.has(h) && !seenHosts.has(h) && !/^(localhost|127\.0\.0\.1|registry\.npmjs\.org|pypi\.org|files\.pythonhosted\.org|github\.com|api\.github\.com|raw\.githubusercontent\.com|killerskills\.ai)$/.test(h)) mk("egress_unexpected", `contacted ${h}; the sandbox saw ${seenHosts.size ? [...seenHosts].slice(0, 3).join(", ") : "no network activity"}${declared.size ? ` and it declares ${[...declared].join(", ")}` : ""}`);
|
|
8486
|
+
for (const p of o.read) if (CRED.test(p) && !seenReads.some((s) => s.includes(p.replace(/^~\//, "")))) mk("credential_read", `read ${p}; the sandbox never saw a credential path read`);
|
|
8487
|
+
for (const p of o.written) {
|
|
8488
|
+
if (MEMORY.test(p) && !trace?.memory_writes.length) mk("memory_write", `wrote ${p}; the sandbox saw no memory or instruction-file writes`);
|
|
8489
|
+
else if (isOutsideProject(p) && !CRED.test(p) && !MEMORY.test(p) && !seenWrites.some((s) => s.includes(p.replace(/^~\//, "")))) mk("outside_project_write", `wrote ${p}, outside the project; not seen in the sandbox`);
|
|
8490
|
+
}
|
|
8491
|
+
return out;
|
|
8492
|
+
}
|
|
8493
|
+
var LOG = join5(HOME, "runtime", "deviations.jsonl");
|
|
8494
|
+
function appendDeviations(ds) {
|
|
8495
|
+
if (!ds.length) return;
|
|
8496
|
+
if (!existsSync5(DIR)) mkdirSync3(DIR, { recursive: true });
|
|
8497
|
+
writeFileSync4(LOG, ds.map((d) => JSON.stringify(d)).join("\n") + "\n", { flag: "a" });
|
|
8498
|
+
}
|
|
8499
|
+
function readDeviations(days = 7) {
|
|
8500
|
+
if (!existsSync5(LOG)) return [];
|
|
8501
|
+
const since = Date.now() - days * 864e5;
|
|
8502
|
+
return readFileSync4(LOG, "utf8").split("\n").filter(Boolean).map((l) => {
|
|
8503
|
+
try {
|
|
8504
|
+
return JSON.parse(l);
|
|
8505
|
+
} catch {
|
|
8506
|
+
return null;
|
|
8507
|
+
}
|
|
8508
|
+
}).filter((d) => !!d && Date.parse(d.at) >= since);
|
|
8509
|
+
}
|
|
8510
|
+
async function observe(stdin) {
|
|
8511
|
+
let payload = {};
|
|
8512
|
+
try {
|
|
8513
|
+
payload = JSON.parse(stdin);
|
|
8514
|
+
} catch {
|
|
8515
|
+
return { code: 0, lines: [] };
|
|
8516
|
+
}
|
|
8517
|
+
const tool = payload.tool_name ?? "";
|
|
8518
|
+
const input = payload.tool_input ?? {};
|
|
8519
|
+
const sid = payload.session_id ?? "default";
|
|
8520
|
+
const session = readSession(sid);
|
|
8521
|
+
const o = parseToolEvent(tool, input);
|
|
8522
|
+
if (o.kind === "Skill") {
|
|
8523
|
+
if (o.skillInvoked) session.active = { skill: o.skillInvoked, since: Date.now() };
|
|
8524
|
+
writeSession(sid, session);
|
|
8525
|
+
return { code: 0, lines: [] };
|
|
8526
|
+
}
|
|
8527
|
+
const skill = attributed(session, tool);
|
|
8528
|
+
if (!skill || !o.hosts.length && !o.read.length && !o.written.length) return { code: 0, lines: [] };
|
|
8529
|
+
const r = await verdictFor(session, skill);
|
|
8530
|
+
const ds = deviations(skill, r, o, tool, payload.session_id ?? null).filter((d) => {
|
|
8531
|
+
const k = `${d.skill}|${d.kind}|${d.detail.split(";")[0]}`;
|
|
8532
|
+
if (session.reported.includes(k)) return false;
|
|
8533
|
+
session.reported.push(k);
|
|
8534
|
+
return true;
|
|
8535
|
+
});
|
|
8536
|
+
writeSession(sid, session);
|
|
8537
|
+
if (!ds.length) return { code: 0, lines: [] };
|
|
8538
|
+
appendDeviations(ds);
|
|
8539
|
+
if (readOrg()) await runtimeReport({ events: ds.map((d) => ({ skill: d.skill, semver: d.semver, kind: d.kind, detail: d.detail, tool: d.tool, at: d.at, session: d.session })) }).catch(() => null);
|
|
8540
|
+
const lines = ds.map((d) => `vet-skills \u26A0 ${d.skill}${d.semver ? `@${d.semver}` : ""} ${d.detail}. Not what its verdict is based on${d.page ? `: https://killerskills.ai${d.page}` : ""}`);
|
|
8541
|
+
const serious = ds.some((d) => d.kind === "egress_unexpected" || d.kind === "credential_read");
|
|
8542
|
+
return { code: serious ? 2 : 0, lines };
|
|
8543
|
+
}
|
|
8544
|
+
function pruneSessions() {
|
|
8545
|
+
if (!existsSync5(DIR)) return;
|
|
8546
|
+
for (const f of readdirSync2(DIR)) {
|
|
8547
|
+
if (!f.endsWith(".json")) continue;
|
|
8548
|
+
const p = join5(DIR, f);
|
|
8549
|
+
try {
|
|
8550
|
+
if (Date.now() - statSync3(p).mtimeMs > 864e5) writeFileSync4(p, "{}");
|
|
8551
|
+
} catch {
|
|
8552
|
+
}
|
|
8553
|
+
}
|
|
8221
8554
|
}
|
|
8222
8555
|
|
|
8223
8556
|
// src/index.ts
|
|
@@ -8228,6 +8561,7 @@ var { values, positionals } = parseArgs({
|
|
|
8228
8561
|
reason: { type: "string" },
|
|
8229
8562
|
org: { type: "string" },
|
|
8230
8563
|
yes: { type: "boolean", short: "y" },
|
|
8564
|
+
lock: { type: "boolean" },
|
|
8231
8565
|
json: { type: "boolean" },
|
|
8232
8566
|
ci: { type: "boolean" },
|
|
8233
8567
|
email: { type: "string" },
|
|
@@ -8265,8 +8599,12 @@ Get started:
|
|
|
8265
8599
|
vet-skills block <skill> [--reason] add to the personal block list
|
|
8266
8600
|
vet-skills status org, policy version, last report, cache size
|
|
8267
8601
|
vet-skills sync pull the org policy and apply pending removals
|
|
8602
|
+
vet-skills verify <skill|path> check the signed verdict against what is on disk
|
|
8603
|
+
vet-skills lock pin every installed skill's hash + attestation in vet-skills.lock
|
|
8604
|
+
vet-skills verify --lock offline: installed skills still match the lock and signatures verify
|
|
8268
8605
|
vet-skills hook install claude-code register the Claude Code PreToolUse hook
|
|
8269
8606
|
vet-skills hook eval (called by the agent) read hook JSON on stdin
|
|
8607
|
+
vet-skills runtime what installed skills did in real sessions that the sandbox never saw
|
|
8270
8608
|
|
|
8271
8609
|
Docs: https://killerskills.ai/docs/cli
|
|
8272
8610
|
`;
|
|
@@ -8344,8 +8682,8 @@ async function main() {
|
|
|
8344
8682
|
usage();
|
|
8345
8683
|
return 1;
|
|
8346
8684
|
}
|
|
8347
|
-
const dir =
|
|
8348
|
-
if (!
|
|
8685
|
+
const dir = resolve2(rest[0]);
|
|
8686
|
+
if (!existsSync6(join6(dir, "SKILL.md")) && !existsSync6(join6(dir, "package.json"))) {
|
|
8349
8687
|
console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md or package.json`);
|
|
8350
8688
|
return 1;
|
|
8351
8689
|
}
|
|
@@ -8387,8 +8725,8 @@ async function main() {
|
|
|
8387
8725
|
console.log(`${c.blocked("\u2717")} not enrolled in an organization. Run ${c.bold("vet-skills join <org> --token \u2026")} first.`);
|
|
8388
8726
|
return 1;
|
|
8389
8727
|
}
|
|
8390
|
-
const dir =
|
|
8391
|
-
if (!
|
|
8728
|
+
const dir = resolve2(rest[0]);
|
|
8729
|
+
if (!existsSync6(join6(dir, "SKILL.md"))) {
|
|
8392
8730
|
console.log(`${c.blocked("\u2717")} ${dir} has no SKILL.md (name and version come from its frontmatter)`);
|
|
8393
8731
|
return 1;
|
|
8394
8732
|
}
|
|
@@ -8432,7 +8770,7 @@ async function main() {
|
|
|
8432
8770
|
console.error("usage: vet-skills join <org> --token <enroll token>");
|
|
8433
8771
|
return 1;
|
|
8434
8772
|
}
|
|
8435
|
-
const machineId =
|
|
8773
|
+
const machineId = createHash2("sha256").update(`${hostname()}:${userInfo().username}`).digest("hex");
|
|
8436
8774
|
const res = await enroll(slug, values.token, {
|
|
8437
8775
|
developer_email: values.email,
|
|
8438
8776
|
machine_id: machineId,
|
|
@@ -8480,7 +8818,7 @@ async function main() {
|
|
|
8480
8818
|
console.error("usage: vet-skills policy set <file.yaml>");
|
|
8481
8819
|
return 1;
|
|
8482
8820
|
}
|
|
8483
|
-
const text =
|
|
8821
|
+
const text = readFileSync5(rest[1], "utf8");
|
|
8484
8822
|
parsePolicyYaml(text);
|
|
8485
8823
|
writePolicy(text);
|
|
8486
8824
|
console.log(`${c.verified("\u2713")} Personal policy updated.`);
|
|
@@ -8506,6 +8844,15 @@ async function main() {
|
|
|
8506
8844
|
console.log(`${c.verified("\u2713")} ${handle} added to the personal block list${values.reason ? ` (${values.reason})` : ""}.`);
|
|
8507
8845
|
return 0;
|
|
8508
8846
|
}
|
|
8847
|
+
case "bug": {
|
|
8848
|
+
const last = readLog().at(-1);
|
|
8849
|
+
const qs = new URLSearchParams({ kind: "bug", source: "cli", cli_version: VERSION, ...last ? { skill: `${last.skill}@${last.semver}`, decision: last.decision } : {} });
|
|
8850
|
+
const url = `${API}/feedback?${qs}`;
|
|
8851
|
+
console.log(`Report a bug: ${url}`);
|
|
8852
|
+
const opener = platform2() === "darwin" ? "open" : platform2() === "win32" ? "start" : "xdg-open";
|
|
8853
|
+
spawnSync(opener, [url], { stdio: "ignore", shell: platform2() === "win32" });
|
|
8854
|
+
return 0;
|
|
8855
|
+
}
|
|
8509
8856
|
case "telemetry": {
|
|
8510
8857
|
const sub = rest[0] ?? "status";
|
|
8511
8858
|
if (sub === "on" || sub === "off") {
|
|
@@ -8541,6 +8888,54 @@ async function main() {
|
|
|
8541
8888
|
}
|
|
8542
8889
|
return 0;
|
|
8543
8890
|
}
|
|
8891
|
+
case "verify": {
|
|
8892
|
+
if (values.lock) {
|
|
8893
|
+
const lock = readLock();
|
|
8894
|
+
if (!lock) {
|
|
8895
|
+
console.log(c.caution(`no ${LOCKFILE} in ${process.cwd()}; run \`vet-skills lock\` first`));
|
|
8896
|
+
return 1;
|
|
8897
|
+
}
|
|
8898
|
+
const rs = verifyLock(lock);
|
|
8899
|
+
for (const r2 of rs) console.log(`${r2.ok ? c.verified("\u2713") : c.blocked("\u2717")} ${r2.subject} ${c.muted(r2.detail)}`);
|
|
8900
|
+
const bad = rs.filter((r2) => !r2.ok).length;
|
|
8901
|
+
console.log(bad ? c.blocked(`${bad} of ${rs.length} failed`) : c.verified(`${rs.length} skill${rs.length === 1 ? "" : "s"} verified against ${LOCKFILE} (offline, ${lock.keys.length} pinned key${lock.keys.length === 1 ? "" : "s"})`));
|
|
8902
|
+
if (values.json) console.log(JSON.stringify(rs, null, 2));
|
|
8903
|
+
return bad ? 1 : 0;
|
|
8904
|
+
}
|
|
8905
|
+
if (!rest[0]) {
|
|
8906
|
+
console.log(c.caution("usage: vet-skills verify <skill>[@version] | <path> | --lock"));
|
|
8907
|
+
return 1;
|
|
8908
|
+
}
|
|
8909
|
+
const keys = await fetchKeys();
|
|
8910
|
+
if (!keys.length) console.log(c.caution(`could not fetch signing keys from ${API}/.well-known/killerskills-keys.json`));
|
|
8911
|
+
const r = await verifyOne(rest[0], keys);
|
|
8912
|
+
console.log(`${r.ok ? c.verified("\u2713 verified") : c.blocked("\u2717 not verified")} ${r.subject}
|
|
8913
|
+
${r.detail}`);
|
|
8914
|
+
if (values.json) console.log(JSON.stringify(r, null, 2));
|
|
8915
|
+
return r.ok ? 0 : 1;
|
|
8916
|
+
}
|
|
8917
|
+
case "lock": {
|
|
8918
|
+
const { file, lock, missing } = await writeLock();
|
|
8919
|
+
console.log(`${c.verified("\u2713")} wrote ${file}: ${lock.skills.length} skill${lock.skills.length === 1 ? "" : "s"}, ${lock.keys.length} signing key${lock.keys.length === 1 ? "" : "s"} pinned`);
|
|
8920
|
+
for (const s of lock.skills) console.log(` ${s.attestation ? c.verified("\u2713") : c.caution("\xB7")} ${s.name} ${c.muted(s.verdict ? `${s.verdict.label} ${s.verdict.score}` : "no attestation (unscored or never crawled)")}`);
|
|
8921
|
+
if (missing.length) console.log(c.muted(`${missing.length} without an attestation are pinned by hash only; \`vet-skills verify --lock\` reports them.`));
|
|
8922
|
+
return 0;
|
|
8923
|
+
}
|
|
8924
|
+
case "runtime": {
|
|
8925
|
+
const ds = readDeviations(7);
|
|
8926
|
+
if (!ds.length) {
|
|
8927
|
+
console.log(c.muted("No deviations in the last 7 days: installed skills behaved the way the sandbox saw them (or no skill ran)."));
|
|
8928
|
+
return 0;
|
|
8929
|
+
}
|
|
8930
|
+
const by = /* @__PURE__ */ new Map();
|
|
8931
|
+
for (const d of ds) by.set(d.skill, [...by.get(d.skill) ?? [], d]);
|
|
8932
|
+
for (const [skill, list] of by) {
|
|
8933
|
+
console.log(`${c.caution("\u26A0")} ${skill}${list[0].semver ? `@${list[0].semver}` : ""} ${c.muted(`${list.length} deviation${list.length === 1 ? "" : "s"}${list[0].page ? ` \xB7 killerskills.ai${list[0].page}` : ""}`)}`);
|
|
8934
|
+
for (const d of list.slice(-6)) console.log(` ${d.at.slice(0, 16).replace("T", " ")} ${d.kind.replace("_", " ")} ${d.detail}`);
|
|
8935
|
+
}
|
|
8936
|
+
if (values.json) console.log(JSON.stringify(ds, null, 2));
|
|
8937
|
+
return 0;
|
|
8938
|
+
}
|
|
8544
8939
|
case "sync": {
|
|
8545
8940
|
if (!readOrg()) {
|
|
8546
8941
|
console.log(c.muted("Not joined to an org; nothing to sync."));
|
|
@@ -8561,6 +8956,18 @@ async function main() {
|
|
|
8561
8956
|
for await (const ch of process.stdin) chunks.push(ch);
|
|
8562
8957
|
return hookEval(Buffer.concat(chunks).toString("utf8"));
|
|
8563
8958
|
}
|
|
8959
|
+
if (sub === "observe") {
|
|
8960
|
+
const chunks = [];
|
|
8961
|
+
for await (const ch of process.stdin) chunks.push(ch);
|
|
8962
|
+
try {
|
|
8963
|
+
const r = await observe(Buffer.concat(chunks).toString("utf8"));
|
|
8964
|
+
for (const l of r.lines) process.stderr.write(l + "\n");
|
|
8965
|
+
if (Math.random() < 0.02) pruneSessions();
|
|
8966
|
+
return r.code;
|
|
8967
|
+
} catch {
|
|
8968
|
+
return 0;
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8564
8971
|
if (sub === "install") {
|
|
8565
8972
|
const target = rest[1] ?? "claude-code";
|
|
8566
8973
|
if (target !== "claude-code") {
|
|
@@ -8569,7 +8976,7 @@ async function main() {
|
|
|
8569
8976
|
}
|
|
8570
8977
|
const r = installClaudeCodeHook();
|
|
8571
8978
|
console.log(r.changed ? `${c.verified("\u2713")} Claude Code hook installed in ${r.path}` : c.muted(`Hook already present in ${r.path}`));
|
|
8572
|
-
console.log(c.muted("Every `skills add`, `claude plugin install`, and `claude mcp add` is now checked before it runs."));
|
|
8979
|
+
console.log(c.muted("Every `skills add`, `claude plugin install`, and `claude mcp add` is now checked before it runs, and what skills do in sessions is compared with their sandbox trace (vet-skills runtime)."));
|
|
8573
8980
|
return 0;
|
|
8574
8981
|
}
|
|
8575
8982
|
usage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vet-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Vet agent skills and MCP servers before they run. Verdicts from KillerSkills, policy from your org.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,14 +10,21 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --banner:js=\"import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\" && node scripts/shebang.mjs",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"test": "node --import tsx --test test/*.test.ts",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
|
+
"prepublishOnly": "pnpm build && node dist/index.js --version"
|
|
19
|
+
},
|
|
13
20
|
"dependencies": {},
|
|
14
21
|
"devDependencies": {
|
|
22
|
+
"@killerskills/policy": "workspace:*",
|
|
15
23
|
"yaml": "^2.6.0",
|
|
16
24
|
"@types/node": "^20",
|
|
17
25
|
"tsx": "^4.19.0",
|
|
18
26
|
"typescript": "^5",
|
|
19
|
-
"esbuild": "^0.28.2"
|
|
20
|
-
"@killerskills/policy": "0.1.0"
|
|
27
|
+
"esbuild": "^0.28.2"
|
|
21
28
|
},
|
|
22
29
|
"engines": {
|
|
23
30
|
"node": ">=20"
|
|
@@ -42,11 +49,5 @@
|
|
|
42
49
|
],
|
|
43
50
|
"publishConfig": {
|
|
44
51
|
"access": "public"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --banner:js=\"import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\" && node scripts/shebang.mjs",
|
|
48
|
-
"dev": "tsx src/index.ts",
|
|
49
|
-
"test": "node --import tsx --test test/*.test.ts",
|
|
50
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
51
52
|
}
|
|
52
|
-
}
|
|
53
|
+
}
|