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.
Files changed (2) hide show
  1. package/dist/index.js +493 -86
  2. 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 SKIP = /* @__PURE__ */ Symbol("skip children");
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 = 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 = 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 ?? sep?.[0],
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 && !sep) {
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(sep ?? [], {
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, sep, null, valueProps, onError);
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 sep = "";
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 += sep + cb;
4177
- sep = "";
4176
+ comment += sep2 + cb;
4177
+ sep2 = "";
4178
4178
  break;
4179
4179
  }
4180
4180
  case "newline":
4181
4181
  if (comment)
4182
- sep += source;
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 ?? sep?.[0],
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 && !sep && !value) {
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 && !sep && !props.found) {
4291
- const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError);
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(sep ?? [], {
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 (sep)
4315
- for (const st of sep) {
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, sep, null, valueProps, onError) : null;
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 sep = "";
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 += sep + indent.slice(trimIndent) + content;
4530
- sep = "\n";
4529
+ value += sep2 + indent.slice(trimIndent) + content;
4530
+ sep2 = "\n";
4531
4531
  } else if (indent.length > trimIndent || content[0] === " ") {
4532
- if (sep === " ")
4533
- sep = "\n";
4534
- else if (!prevMoreIndented && sep === "\n")
4535
- sep = "\n\n";
4536
- value += sep + indent.slice(trimIndent) + content;
4537
- sep = "\n";
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 (sep === "\n")
4540
+ if (sep2 === "\n")
4541
4541
  value += "\n";
4542
4542
  else
4543
- sep = "\n";
4543
+ sep2 = "\n";
4544
4544
  } else {
4545
- value += sep + content;
4546
- sep = " ";
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 sep = " ";
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 (sep === "\n")
4735
- res += sep;
4734
+ if (sep2 === "\n")
4735
+ res += sep2;
4736
4736
  else
4737
- sep = "\n";
4737
+ sep2 = "\n";
4738
4738
  } else {
4739
- res += sep + lm;
4740
- sep = " ";
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 + sep + (match?.[1] ?? "");
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 (sep)
5564
- for (const st of sep)
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 SKIP = /* @__PURE__ */ Symbol("skip children");
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 = 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 sep;
6731
+ let sep2;
6732
6732
  if (scalar.end) {
6733
- sep = scalar.end;
6734
- sep.push(this.sourceToken);
6733
+ sep2 = scalar.end;
6734
+ sep2.push(this.sourceToken);
6735
6735
  delete scalar.end;
6736
6736
  } else
6737
- sep = [this.sourceToken];
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 sep = it.sep;
6896
- sep.push(this.sourceToken);
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 sep = fc.end.splice(1, fc.end.length);
7098
- sep.push(this.sourceToken);
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 readFileSync3, existsSync as existsSync4 } from "node:fs";
7618
- import { resolve, join as join4, dirname, basename } from "node:path";
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.5.1";
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 existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
8032
- import { homedir as homedir3 } from "node:os";
8033
- import { join as join3 } from "node:path";
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 existsSync2, rmSync } from "node:fs";
8037
- import { homedir as homedir2 } from "node:os";
8038
- import { join as join2 } from "node:path";
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 [join2(homedir2(), ".claude", "skills", bare), join2(process.cwd(), ".claude", "skills", bare), join2(homedir2(), ".claude", "plugins", bare)];
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) => existsSync2(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 = join3(homedir3(), ".claude");
8204
- const path = join3(dir, "settings.json");
8205
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
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 (existsSync3(path)) {
8363
+ if (existsSync4(path)) {
8208
8364
  try {
8209
- settings = JSON.parse(readFileSync2(path, "utf8"));
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 already = pre.some((h) => h.hooks?.some((x) => /vet-skills hook eval/.test(x.command)));
8217
- if (already) return { path, changed: false };
8218
- pre.push({ matcher: "Bash", hooks: [{ type: "command", command: "vet-skills hook eval" }] });
8219
- writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n");
8220
- return { path, changed: true };
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 = resolve(rest[0]);
8348
- if (!existsSync4(join4(dir, "SKILL.md")) && !existsSync4(join4(dir, "package.json"))) {
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 = resolve(rest[0]);
8391
- if (!existsSync4(join4(dir, "SKILL.md"))) {
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 = createHash("sha256").update(`${hostname()}:${userInfo().username}`).digest("hex");
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 = readFileSync3(rest[1], "utf8");
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.5.1",
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
+ }