@hasna/skills 0.10.39 → 0.10.41

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/README.md CHANGED
@@ -2418,3 +2418,29 @@ Migration compares the original witness before projecting the new contract,
2418
2418
  refuses native discovery or unknown control changes, and changes policy only.
2419
2419
  After explicit migration, ordinary supported local MCP updates require no
2420
2420
  coordinated policy write.
2421
+
2422
+ ### Reproduce a reviewed runtime dependency closure
2423
+
2424
+ For an exact-version copyfile update, supply an owner-private absolute npm
2425
+ lockfile path and its reviewed SHA-256 together:
2426
+
2427
+ ```sh
2428
+ skills self-update --version <version> --reviewed-lock /absolute/package-lock.json \
2429
+ --reviewed-lock-sha256 <sha256> --min-release-age 7 --json
2430
+ ```
2431
+
2432
+ Use the same lock for a separately reviewed bootstrap. The lock must describe
2433
+ the verified package archive as `file:./verified.tgz`, use lockfile format 3,
2434
+ and bind its version, integrity and dependency declarations. Registry packages
2435
+ must have exact versions and SHA-512 integrity at the configured npm registry.
2436
+ The updater verifies each frozen version against current registry metadata
2437
+ and the requested age policy, runs `npm ci` without re-resolving ranges, then
2438
+ checks lock byte continuity and the actual required dependency closure with
2439
+ `npm sbom --sbom-format=cyclonedx --package-lock-only=false`. This forces
2440
+ installed-manifest inspection; ordinary `npm ls` can trust a hidden lockfile.
2441
+ Existing publisher exemptions remain available through the repeatable
2442
+ `--min-release-age-exclude` option. The receipt records the reviewed lock hash.
2443
+
2444
+ A reviewed lock does not authorize native skill loading, profile changes,
2445
+ credential provisioning or session reconciliation. Without this optional input,
2446
+ the existing update flow continues to resolve its dependency lock normally.
package/bin/index.js CHANGED
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.10.39",
36871
+ version: "0.10.41",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -110034,8 +110034,139 @@ var init_remote_account2 = __esm(() => {
110034
110034
  init_remote_client();
110035
110035
  });
110036
110036
 
110037
+ // src/cli/commands/reviewed-runtime-lock.ts
110038
+ import { createHash as createHash30 } from "crypto";
110039
+ function row(value) {
110040
+ if (!value || typeof value !== "object" || Array.isArray(value))
110041
+ throw new Error("REVIEWED_LOCK_SHAPE_INVALID");
110042
+ return value;
110043
+ }
110044
+ function same4(a, b) {
110045
+ const entries = (v) => Object.entries(row(v ?? {})).sort(([a2], [b2]) => a2.localeCompare(b2));
110046
+ return JSON.stringify(entries(a)) === JSON.stringify(entries(b));
110047
+ }
110048
+ function packageName(path) {
110049
+ const parts = path.split("/node_modules/");
110050
+ if (!parts[0].startsWith("node_modules/"))
110051
+ throw new Error("REVIEWED_LOCK_LOCATION_INVALID");
110052
+ parts[0] = parts[0].slice("node_modules/".length);
110053
+ if (parts.some((name) => !NAME.test(name) || name.split("/").some((part) => part === "." || part === "..")))
110054
+ throw new Error("REVIEWED_LOCK_LOCATION_INVALID");
110055
+ return parts.at(-1);
110056
+ }
110057
+ function excluded(name, patterns) {
110058
+ return patterns.some((pattern) => new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`).test(name));
110059
+ }
110060
+ async function metadata(name, context2) {
110061
+ const url2 = new URL(encodeURIComponent(name), `${context2.registryOrigin}/`);
110062
+ const response = await (context2.fetcher ?? fetch)(url2, { redirect: "error", signal: AbortSignal.timeout(30000) });
110063
+ if (!response.ok || !response.body)
110064
+ throw new Error("REVIEWED_LOCK_METADATA_UNAVAILABLE");
110065
+ const reader = response.body.getReader();
110066
+ const chunks = [];
110067
+ let size2 = 0;
110068
+ try {
110069
+ for (;; ) {
110070
+ const { done, value } = await reader.read();
110071
+ if (done)
110072
+ break;
110073
+ size2 += value.byteLength;
110074
+ if (size2 > MAX_METADATA_BYTES) {
110075
+ await reader.cancel();
110076
+ throw new Error("REVIEWED_LOCK_METADATA_TOO_LARGE");
110077
+ }
110078
+ chunks.push(value);
110079
+ }
110080
+ } finally {
110081
+ reader.releaseLock();
110082
+ }
110083
+ try {
110084
+ return row(JSON.parse(Buffer.concat(chunks).toString("utf8")));
110085
+ } catch {
110086
+ throw new Error("REVIEWED_LOCK_METADATA_INVALID");
110087
+ }
110088
+ }
110089
+ async function validateReviewedRuntimeLock(bytes, sha2562, context2) {
110090
+ if (!/^[a-f0-9]{64}$/.test(sha2562))
110091
+ throw new Error("REVIEWED_LOCK_SHA256_INVALID");
110092
+ if (bytes.byteLength > MAX_LOCK_BYTES || createHash30("sha256").update(bytes).digest("hex") !== sha2562)
110093
+ throw new Error("REVIEWED_LOCK_HASH_MISMATCH");
110094
+ if (!Number.isSafeInteger(context2.minReleaseAge) || context2.minReleaseAge < 1)
110095
+ throw new Error("REVIEWED_LOCK_RELEASE_AGE_REQUIRED");
110096
+ let lock;
110097
+ try {
110098
+ lock = row(JSON.parse(Buffer.from(bytes).toString("utf8")));
110099
+ } catch {
110100
+ throw new Error("REVIEWED_LOCK_SHAPE_INVALID");
110101
+ }
110102
+ if (lock.lockfileVersion !== 3 || lock.requires !== true)
110103
+ throw new Error("REVIEWED_LOCK_FORMAT_UNSUPPORTED");
110104
+ const packages = row(lock.packages);
110105
+ if (Object.keys(packages).length > 5000)
110106
+ throw new Error("REVIEWED_LOCK_POPULATION_TOO_LARGE");
110107
+ const root2 = row(packages[""]);
110108
+ if (!same4(root2.dependencies, { [PACKAGE2]: "file:./verified.tgz" }) || Object.keys(row(root2.devDependencies ?? {})).length || Object.keys(row(root2.optionalDependencies ?? {})).length)
110109
+ throw new Error("REVIEWED_LOCK_ROOT_MISMATCH");
110110
+ const target = row(packages[`node_modules/${PACKAGE2}`]);
110111
+ if (target.version !== context2.version || target.resolved !== "file:verified.tgz" || target.integrity !== context2.archiveIntegrity || target.name !== undefined && target.name !== PACKAGE2)
110112
+ throw new Error("REVIEWED_LOCK_ARCHIVE_MISMATCH");
110113
+ for (const field2 of ["dependencies", "optionalDependencies", "peerDependencies"]) {
110114
+ if (!same4(target[field2], context2.packageManifest[field2]))
110115
+ throw new Error("REVIEWED_LOCK_MANIFEST_MISMATCH");
110116
+ }
110117
+ const registry2 = new URL(context2.registryOrigin);
110118
+ const identities = new Map;
110119
+ for (const [location, value] of Object.entries(packages)) {
110120
+ if (!location)
110121
+ continue;
110122
+ const name = packageName(location), entry = row(value);
110123
+ if (entry.link || entry.dev || entry.name !== undefined && entry.name !== name)
110124
+ throw new Error("REVIEWED_LOCK_LOCATION_INVALID");
110125
+ if (location === `node_modules/${PACKAGE2}`)
110126
+ continue;
110127
+ if (typeof entry.version !== "string" || !VERSION.test(entry.version) || typeof entry.resolved !== "string" || typeof entry.integrity !== "string" || !INTEGRITY.test(entry.integrity))
110128
+ throw new Error("REVIEWED_LOCK_PACKAGE_INVALID");
110129
+ let url2;
110130
+ try {
110131
+ url2 = new URL(entry.resolved);
110132
+ } catch {
110133
+ throw new Error("REVIEWED_LOCK_REGISTRY_INVALID");
110134
+ }
110135
+ if (url2.origin !== registry2.origin || url2.username || url2.password || url2.search || url2.hash || !url2.pathname.endsWith(".tgz"))
110136
+ throw new Error("REVIEWED_LOCK_REGISTRY_INVALID");
110137
+ const entries = identities.get(name) ?? [];
110138
+ entries.push({ name, version: entry.version, resolved: entry.resolved, integrity: entry.integrity });
110139
+ identities.set(name, entries);
110140
+ }
110141
+ const names = [...identities.keys()];
110142
+ const now3 = context2.now ?? Date.now();
110143
+ for (let offset = 0;offset < names.length; offset += 6) {
110144
+ await Promise.all(names.slice(offset, offset + 6).map(async (name) => {
110145
+ const data = await metadata(name, context2), versions2 = row(data.versions), times2 = row(data.time);
110146
+ if (data.name !== name)
110147
+ throw new Error("REVIEWED_LOCK_METADATA_IDENTITY_MISMATCH");
110148
+ for (const entry of identities.get(name)) {
110149
+ const version2 = row(versions2[entry.version]), dist = row(version2.dist);
110150
+ if (version2.name !== name || version2.version !== entry.version || dist.integrity !== entry.integrity || dist.tarball !== entry.resolved)
110151
+ throw new Error("REVIEWED_LOCK_METADATA_IDENTITY_MISMATCH");
110152
+ const published = typeof times2[entry.version] === "string" ? Date.parse(times2[entry.version]) : NaN;
110153
+ if (!Number.isFinite(published) || published > now3 || !excluded(name, context2.minReleaseAgeExclude) && now3 - published < context2.minReleaseAge * 86400000)
110154
+ throw new Error("REVIEWED_LOCK_RELEASE_AGE_REFUSED");
110155
+ }
110156
+ }));
110157
+ }
110158
+ }
110159
+ var PACKAGE2 = "@hasna/skills", NAME, VERSION, INTEGRITY, MAX_LOCK_BYTES, MAX_METADATA_BYTES;
110160
+ var init_reviewed_runtime_lock = __esm(() => {
110161
+ NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
110162
+ VERSION = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/;
110163
+ INTEGRITY = /^sha512-[A-Za-z0-9+/]{86}==$/;
110164
+ MAX_LOCK_BYTES = 8 * 1024 * 1024;
110165
+ MAX_METADATA_BYTES = 32 * 1024 * 1024;
110166
+ });
110167
+
110037
110168
  // src/cli/commands/runtime-copyfile.ts
110038
- import { createHash as createHash30, randomUUID as randomUUID9 } from "crypto";
110169
+ import { createHash as createHash31, randomUUID as randomUUID9 } from "crypto";
110039
110170
  import { Readable } from "stream";
110040
110171
  import { createGunzip as createGunzip2 } from "zlib";
110041
110172
  import {
@@ -110059,7 +110190,7 @@ function object9(value, code2) {
110059
110190
  return value;
110060
110191
  }
110061
110192
  function hash5(bytes) {
110062
- return createHash30("sha256").update(bytes).digest("hex");
110193
+ return createHash31("sha256").update(bytes).digest("hex");
110063
110194
  }
110064
110195
  function isWithin(parent, child) {
110065
110196
  const rel = relative10(parent, child);
@@ -110210,7 +110341,7 @@ function verifyIntegrity(bytes, integrity) {
110210
110341
  const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/.exec(integrity);
110211
110342
  if (!match)
110212
110343
  throw new Error("REGISTRY_INTEGRITY_UNSUPPORTED");
110213
- const actual = createHash30("sha512").update(bytes).digest("base64");
110344
+ const actual = createHash31("sha512").update(bytes).digest("base64");
110214
110345
  if (actual !== match[1])
110215
110346
  throw new Error("TARBALL_INTEGRITY_MISMATCH");
110216
110347
  }
@@ -110322,10 +110453,10 @@ async function downloadPackage(version2, stagingRoot, registryOrigin, fetcher) {
110322
110453
  const metadataResponse = await fetcher(metadataUrl, { redirect: "error", signal: timeout });
110323
110454
  if (!metadataResponse.ok || new URL(metadataResponse.url || metadataUrl).origin !== new URL(registryOrigin).origin)
110324
110455
  throw new Error("REGISTRY_METADATA_FETCH_FAILED");
110325
- const metadata = object9(JSON.parse(new TextDecoder().decode(await readBodyCapped(metadataResponse, 5 * 1024 * 1024, "REGISTRY_METADATA_SIZE_INVALID"))), "REGISTRY_METADATA_INVALID");
110326
- if (metadata.name !== PACKAGE_NAME || metadata.version !== version2 || metadata._id !== `${PACKAGE_NAME}@${version2}`)
110456
+ const metadata2 = object9(JSON.parse(new TextDecoder().decode(await readBodyCapped(metadataResponse, 5 * 1024 * 1024, "REGISTRY_METADATA_SIZE_INVALID"))), "REGISTRY_METADATA_INVALID");
110457
+ if (metadata2.name !== PACKAGE_NAME || metadata2.version !== version2 || metadata2._id !== `${PACKAGE_NAME}@${version2}`)
110327
110458
  throw new Error("REGISTRY_PACKAGE_IDENTITY_MISMATCH");
110328
- const dist = object9(metadata.dist, "REGISTRY_DIST_INVALID");
110459
+ const dist = object9(metadata2.dist, "REGISTRY_DIST_INVALID");
110329
110460
  const integrity = typeof dist.integrity === "string" ? dist.integrity : "";
110330
110461
  const tarballUrl = expectedTarballUrl(dist.tarball, registryOrigin, version2);
110331
110462
  const response = await fetcher(tarballUrl, { redirect: "error", signal: AbortSignal.timeout(30000) });
@@ -110996,6 +111127,24 @@ async function updateCopyfileRuntime(version2, options = {}) {
110996
111127
  if (!STABLE_SEMVER_PATTERN.test(version2))
110997
111128
  throw new Error("EXACT_STABLE_VERSION_REQUIRED");
110998
111129
  const policy = releaseAgePolicy(options);
111130
+ let reviewedLockBytes;
111131
+ if (options.reviewedLock !== undefined || options.reviewedLockSha256 !== undefined) {
111132
+ if (!options.reviewedLock || !options.reviewedLockSha256)
111133
+ throw new Error("REVIEWED_LOCK_INPUT_PAIR_REQUIRED");
111134
+ if (!policy)
111135
+ throw new Error("REVIEWED_LOCK_RELEASE_AGE_REQUIRED");
111136
+ if (!isAbsolute25(options.reviewedLock))
111137
+ throw new Error("REVIEWED_LOCK_ABSOLUTE_PATH_REQUIRED");
111138
+ assertNoSymlinkAncestors(dirname31(options.reviewedLock));
111139
+ const stat3 = lstatSync30(options.reviewedLock);
111140
+ if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.nlink !== 1 || stat3.uid !== process.getuid?.() || stat3.mode & 63 || stat3.size > 8 * 1024 * 1024)
111141
+ throw new Error("REVIEWED_LOCK_FILE_UNSAFE");
111142
+ reviewedLockBytes = readFileSync38(options.reviewedLock);
111143
+ if (!/^[a-f0-9]{64}$/.test(options.reviewedLockSha256))
111144
+ throw new Error("REVIEWED_LOCK_SHA256_INVALID");
111145
+ if (hash5(reviewedLockBytes) !== options.reviewedLockSha256)
111146
+ throw new Error("REVIEWED_LOCK_HASH_MISMATCH");
111147
+ }
110999
111148
  const home = resolve25(options.homeDir ?? process.env.HOME ?? "");
111000
111149
  if (!home || !existsSync34(home))
111001
111150
  throw new Error("HOME_NOT_FOUND");
@@ -111032,12 +111181,37 @@ async function updateCopyfileRuntime(version2, options = {}) {
111032
111181
  const stagePackage = join58(stagePath, "install");
111033
111182
  mkdirSync24(stagePackage, { mode: 448 });
111034
111183
  copyFileSync(artifact.tarballPath, join58(stagePackage, "verified.tgz"));
111035
- writeFileSync23(join58(stagePackage, "package.json"), JSON.stringify({ private: true, dependencies: { [PACKAGE_NAME]: "file:./verified.tgz" } }, null, 2), { mode: 384, flag: "wx" });
111036
- const npmVersion = await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
111184
+ writeFileSync23(join58(stagePackage, "package.json"), JSON.stringify({ name: "skills-runtime-install", version: "0.0.0", private: true, dependencies: { [PACKAGE_NAME]: "file:./verified.tgz" } }, null, 2), { mode: 384, flag: "wx" });
111185
+ let npmVersion;
111186
+ if (reviewedLockBytes) {
111187
+ await validateReviewedRuntimeLock(reviewedLockBytes, options.reviewedLockSha256, {
111188
+ version: version2,
111189
+ archiveIntegrity: artifact.integrity,
111190
+ packageManifest: object9(JSON.parse(readFileSync38(join58(stagePath, "verified-archive", "package", "package.json"), "utf8")), "PACKAGE_MANIFEST_INVALID"),
111191
+ registryOrigin: REGISTRY_ORIGIN,
111192
+ ...policy,
111193
+ fetcher
111194
+ });
111195
+ writeFileSync23(join58(stagePackage, "package-lock.json"), reviewedLockBytes, { mode: 384, flag: "wx" });
111196
+ if (!readFileSync38(join58(stagePackage, "package-lock.json")).equals(reviewedLockBytes))
111197
+ throw new Error("REVIEWED_LOCK_COPY_MISMATCH");
111198
+ } else {
111199
+ npmVersion = await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
111200
+ }
111037
111201
  const ciNpmVersion = await runNpm(["ci", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
111038
- if (npmVersion !== ciNpmVersion)
111202
+ if (!reviewedLockBytes && npmVersion !== ciNpmVersion)
111039
111203
  throw new Error("NPM_VERSION_DRIFT_DURING_UPDATE");
111204
+ npmVersion = ciNpmVersion;
111040
111205
  const lockBytes = readFileSync38(join58(stagePackage, "package-lock.json"));
111206
+ if (reviewedLockBytes) {
111207
+ if (!lockBytes.equals(reviewedLockBytes))
111208
+ throw new Error("REVIEWED_LOCK_DRIFT_DURING_INSTALL");
111209
+ const treeNpmVersion = await runNpm(["sbom", "--sbom-format=cyclonedx", "--package-lock-only=false", "--omit=dev", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
111210
+ if (treeNpmVersion !== npmVersion)
111211
+ throw new Error("NPM_VERSION_DRIFT_DURING_UPDATE");
111212
+ if (!readFileSync38(join58(stagePackage, "package-lock.json")).equals(reviewedLockBytes))
111213
+ throw new Error("REVIEWED_LOCK_DRIFT_DURING_INSTALL");
111214
+ }
111041
111215
  const installedPackage = join58(stagePackage, "node_modules", "@hasna", "skills");
111042
111216
  const installed = readPackage(join58(installedPackage, "package.json"));
111043
111217
  if (installed.version !== version2 || JSON.stringify(Object.entries(installed.bins).sort()) !== JSON.stringify(Object.entries(layout.bin).sort())) {
@@ -111078,6 +111252,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
111078
111252
  configs: configPreimages,
111079
111253
  preimageSha256,
111080
111254
  ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {},
111255
+ ...reviewedLockBytes ? { reviewedLockSha256: options.reviewedLockSha256 } : {},
111081
111256
  launchers,
111082
111257
  switchedLaunchers: [],
111083
111258
  rollbackCompletedLaunchers: []
@@ -111116,7 +111291,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
111116
111291
  }
111117
111292
  receipt.state = "switched";
111118
111293
  persistReceipt(join58(finalPath, "rollout-receipt.json"), receipt);
111119
- return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length, ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {} };
111294
+ return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length, ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {}, ...reviewedLockBytes ? { reviewedLockSha256: options.reviewedLockSha256 } : {} };
111120
111295
  } catch (error61) {
111121
111296
  if (switched.length > 0 || moved) {
111122
111297
  let rollbackFailed = false;
@@ -111253,6 +111428,7 @@ function rollbackCopyfileRuntime(receiptId, options = {}) {
111253
111428
  var PACKAGE_NAME = "@hasna/skills", REGISTRY_ORIGIN = "https://registry.npmjs.org", STABLE_SEMVER_PATTERN, MAX_TARBALL_BYTES, MAX_TARBALL_EXPANDED_BYTES, MAX_TARBALL_ENTRIES = 20000, CONFIG_PREIMAGE_PATHS;
111254
111429
  var init_runtime_copyfile = __esm(() => {
111255
111430
  init_skill_contract();
111431
+ init_reviewed_runtime_lock();
111256
111432
  STABLE_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
111257
111433
  MAX_TARBALL_BYTES = 100 * 1024 * 1024;
111258
111434
  MAX_TARBALL_EXPANDED_BYTES = 256 * 1024 * 1024;
@@ -111269,7 +111445,7 @@ var exports_runtime = {};
111269
111445
  __export(exports_runtime, {
111270
111446
  registerRuntime: () => registerRuntime
111271
111447
  });
111272
- import { createHash as createHash31, randomUUID as randomUUID10 } from "crypto";
111448
+ import { createHash as createHash32, randomUUID as randomUUID10 } from "crypto";
111273
111449
  import { lstatSync as lstatSync31, mkdirSync as mkdirSync25, readFileSync as readFileSync39, realpathSync as realpathSync17, writeFileSync as writeFileSync24 } from "fs";
111274
111450
  import { basename as basename16, isAbsolute as isAbsolute26, join as join59 } from "path";
111275
111451
  import { createInterface } from "readline";
@@ -111310,7 +111486,7 @@ function registerRuntime(parent) {
111310
111486
  if (!expected)
111311
111487
  throw new Error("Cloud artifact not found");
111312
111488
  const bytes = await client2.download(id2, name);
111313
- if (bytes.length !== expected.byteSize || createHash31("sha256").update(bytes).digest("hex") !== expected.sha256)
111489
+ if (bytes.length !== expected.byteSize || createHash32("sha256").update(bytes).digest("hex") !== expected.sha256)
111314
111490
  throw new Error("Cloud artifact integrity verification failed");
111315
111491
  writeFileSync24(options.output, bytes, { flag: "wx", mode: 384 });
111316
111492
  console.log(JSON.stringify({ path: options.output, sha256: expected.sha256, byteSize: bytes.length }));
@@ -111340,7 +111516,12 @@ function registerRuntime(parent) {
111340
111516
  });
111341
111517
  const setup = parent.command("setup").description("Point this CLI at a Skills API server, or register agent integrations").option("--api-url <url>", "Skills API origin to send remote work to").option("--global", "Accepted for compatibility; the API origin is always per-user", false).option("--json", "Output setup result as JSON", false).action(async (options) => handleSetup(options));
111342
111518
  setup.command("agents").option("--json", "Output registration result as JSON", false).description("Register the Skills MCP server with all supported agents").action(async (options) => handleMcp({ register: "all", json: options.json }));
111343
- parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--min-release-age <days>", "Require a positive integer dependency release age in days for an exact-version update").option("--min-release-age-exclude <pattern>", "Exclude matching package identities from the release-age policy (repeatable)", (pattern, prior) => [...prior, pattern], []).option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
111519
+ parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--min-release-age <days>", "Require a positive integer dependency release age in days for an exact-version update").option("--min-release-age-exclude <pattern>", "Exclude matching package identities from the release-age policy (repeatable)", (pattern, prior) => [...prior, pattern], []).option("--reviewed-lock <path>", "Install from this private absolute package-lock.json path without resolving dependency ranges").option("--reviewed-lock-sha256 <sha256>", "Exact reviewed SHA-256 of the supplied package-lock.json").option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
111520
+ if ((options.reviewedLock !== undefined || options.reviewedLockSha256 !== undefined) && (!options.version || options.rollback || options.adoptAliases || options.rollbackAliases)) {
111521
+ console.error(JSON.stringify({ error: "REVIEWED_LOCK_REQUIRES_EXACT_VERSION" }));
111522
+ process.exitCode = 1;
111523
+ return;
111524
+ }
111344
111525
  if ((options.minReleaseAge !== undefined || options.minReleaseAgeExclude.length > 0) && (!options.version || options.rollback || options.adoptAliases || options.rollbackAliases)) {
111345
111526
  console.error(JSON.stringify({ error: "RELEASE_AGE_POLICY_REQUIRES_EXACT_VERSION" }));
111346
111527
  process.exitCode = 1;
@@ -111382,7 +111563,7 @@ function registerRuntime(parent) {
111382
111563
  if (options.version) {
111383
111564
  try {
111384
111565
  const minReleaseAge = options.minReleaseAge === undefined ? undefined : /^\d+$/.test(options.minReleaseAge) ? Number(options.minReleaseAge) : NaN;
111385
- const result2 = await updateCopyfileRuntime(options.version, { minReleaseAge, minReleaseAgeExclude: options.minReleaseAgeExclude });
111566
+ const result2 = await updateCopyfileRuntime(options.version, { minReleaseAge, minReleaseAgeExclude: options.minReleaseAgeExclude, reviewedLock: options.reviewedLock, reviewedLockSha256: options.reviewedLockSha256 });
111386
111567
  if (options.json)
111387
111568
  console.log(JSON.stringify(result2));
111388
111569
  else
@@ -112959,7 +113140,7 @@ var init_portable_snapshot_filter = __esm(() => {
112959
113140
  });
112960
113141
 
112961
113142
  // src/lib/station-snapshot.ts
112962
- import { createHash as createHash32 } from "crypto";
113143
+ import { createHash as createHash33 } from "crypto";
112963
113144
  import {
112964
113145
  copyFileSync as copyFileSync2,
112965
113146
  mkdirSync as mkdirSync26,
@@ -112974,7 +113155,7 @@ function validateStationId(stationId) {
112974
113155
  }
112975
113156
  }
112976
113157
  function sha256File(filePath) {
112977
- return createHash32("sha256").update(readFileSync40(filePath)).digest("hex");
113158
+ return createHash33("sha256").update(readFileSync40(filePath)).digest("hex");
112978
113159
  }
112979
113160
  function scanHome(definition, homesRoot) {
112980
113161
  const homePath = homePathFor(definition, homesRoot);
@@ -113664,7 +113845,7 @@ var init_codex_hook_policy_recovery = __esm(() => {
113664
113845
 
113665
113846
  // src/lib/agent-codex-trust.ts
113666
113847
  import { lstatSync as lstatSync33, mkdirSync as mkdirSync27, readdirSync as readdirSync23, readFileSync as readFileSync41, realpathSync as realpathSync19, statSync as statSync22 } from "fs";
113667
- import { createHash as createHash33, randomUUID as randomUUID11 } from "crypto";
113848
+ import { createHash as createHash34, randomUUID as randomUUID11 } from "crypto";
113668
113849
  import { execFileSync as execFileSync2 } from "child_process";
113669
113850
  import { dirname as dirname35, isAbsolute as isAbsolute29, join as join63, resolve as resolve27 } from "path";
113670
113851
  import { homedir as homedir12 } from "os";
@@ -113674,11 +113855,11 @@ function verifyReleaseArtifact(proof, skillsCli) {
113674
113855
  const tarPath = realpathSync19(proof.packageTarPath);
113675
113856
  need5(statSync22(tarPath).isFile(), "RECONCILE_RELEASE_PROVENANCE_PATH");
113676
113857
  const tarBytes = readFileSync41(tarPath);
113677
- need5(tarBytes.byteLength === proof.packageTarBytes && createHash33("sha256").update(tarBytes).digest("hex") === proof.packageTarSha256, "RECONCILE_RELEASE_ARTIFACT_CHANGED");
113858
+ need5(tarBytes.byteLength === proof.packageTarBytes && createHash34("sha256").update(tarBytes).digest("hex") === proof.packageTarSha256, "RECONCILE_RELEASE_ARTIFACT_CHANGED");
113678
113859
  const packageRoot = dirname35(dirname35(realpathSync19(skillsCli.receipt.path)));
113679
113860
  const manifest = readFileSync41(join63(packageRoot, "package.json"));
113680
- need5(createHash33("sha256").update(manifest).digest("hex") === skillsCli.receipt.manifestSha256 && skillsCli.receipt.manifestSha256 === proof.manifestSha256, "RECONCILE_RELEASE_MANIFEST_CHANGED");
113681
- need5(createHash33("sha256").update(readFileSync41(skillsCli.receipt.path)).digest("hex") === proof.executableSha256 && skillsCli.receipt.sha256 === proof.executableSha256, "RECONCILE_RELEASE_EXECUTABLE_CHANGED");
113861
+ need5(createHash34("sha256").update(manifest).digest("hex") === skillsCli.receipt.manifestSha256 && skillsCli.receipt.manifestSha256 === proof.manifestSha256, "RECONCILE_RELEASE_MANIFEST_CHANGED");
113862
+ need5(createHash34("sha256").update(readFileSync41(skillsCli.receipt.path)).digest("hex") === proof.executableSha256 && skillsCli.receipt.sha256 === proof.executableSha256, "RECONCILE_RELEASE_EXECUTABLE_CHANGED");
113682
113863
  const members2 = [["package/package.json", manifest], ["package/bin/index.js", readFileSync41(skillsCli.receipt.path)]];
113683
113864
  for (const [member, expected] of members2) {
113684
113865
  let actual;
@@ -113996,7 +114177,7 @@ async function enrollCodexNativeHooksUnlocked(options, connect) {
113996
114177
  }
113997
114178
  }
113998
114179
  var json2 = (value) => JSON.stringify(value, null, 2) + `
113999
- `, structure3 = (text4) => JSON.parse(JSON.stringify(Bun.TOML.parse(text4))), eventNames, makePlanDigest = (value) => createHash33("sha256").update(json2(value)).digest("hex"), safeNativeHook = (hook) => ({ key: hook.key, eventName: hook.eventName, handlerType: hook.handlerType, command: hook.command, matcher: hook.matcher, timeoutSec: hook.timeoutSec, async: hook.async, statusMessage: hook.statusMessage, additionalContextLimit: hook.additionalContextLimit, sourcePath: hook.sourcePath, source: hook.source, pluginId: hook.pluginId, isManaged: hook.isManaged, currentHash: hook.currentHash, enabled: hook.enabled, trustStatus: hook.trustStatus }), withoutCodexCommand = (value) => {
114180
+ `, structure3 = (text4) => JSON.parse(JSON.stringify(Bun.TOML.parse(text4))), eventNames, makePlanDigest = (value) => createHash34("sha256").update(json2(value)).digest("hex"), safeNativeHook = (hook) => ({ key: hook.key, eventName: hook.eventName, handlerType: hook.handlerType, command: hook.command, matcher: hook.matcher, timeoutSec: hook.timeoutSec, async: hook.async, statusMessage: hook.statusMessage, additionalContextLimit: hook.additionalContextLimit, sourcePath: hook.sourcePath, source: hook.source, pluginId: hook.pluginId, isManaged: hook.isManaged, currentHash: hook.currentHash, enabled: hook.enabled, trustStatus: hook.trustStatus }), withoutCodexCommand = (value) => {
114000
114181
  const copy = structuredClone(value);
114001
114182
  if (copy?.bridge?.commands && Object.hasOwn(copy.bridge.commands, "codex"))
114002
114183
  copy.bridge.commands.codex = "<reviewed-codex-binding>";
@@ -114138,7 +114319,7 @@ __export(exports_agent_integration, {
114138
114319
  registerAgentIntegration: () => registerAgentIntegration
114139
114320
  });
114140
114321
  import { existsSync as existsSync36, lstatSync as lstatSync34, readFileSync as readFileSync42, readSync as readSync17, writeFileSync as writeFileSync26 } from "fs";
114141
- import { createHash as createHash34 } from "crypto";
114322
+ import { createHash as createHash35 } from "crypto";
114142
114323
  import { homedir as homedir13 } from "os";
114143
114324
  import { isAbsolute as isAbsolute30 } from "path";
114144
114325
  function pinnedHookProfile(input3, configuredProfile) {
@@ -114263,7 +114444,7 @@ function registerAgentIntegration(parent) {
114263
114444
  skillCount: catalog.skills.length,
114264
114445
  output: options.output,
114265
114446
  bytes: Buffer.byteLength(bytes),
114266
- sha256: createHash34("sha256").update(bytes).digest("hex")
114447
+ sha256: createHash35("sha256").update(bytes).digest("hex")
114267
114448
  };
114268
114449
  await writeCliOutput(options.json ? JSON.stringify(receipt) : `Captured ${receipt.skillCount} native Codex skill(s) in ${receipt.output}.`);
114269
114450
  } catch (error61) {
@@ -114779,7 +114960,7 @@ var init_grants = __esm(() => {
114779
114960
  });
114780
114961
 
114781
114962
  // src/lib/station-hydrate.ts
114782
- import { createHash as createHash35 } from "crypto";
114963
+ import { createHash as createHash36 } from "crypto";
114783
114964
  import {
114784
114965
  copyFileSync as copyFileSync3,
114785
114966
  mkdirSync as mkdirSync28,
@@ -114982,7 +115163,7 @@ function skillSha256(skill) {
114982
115163
  return sha256File(skill.files[0].winner.fullPath);
114983
115164
  }
114984
115165
  const joined = skill.files.map((file2) => sha256File(file2.winner.fullPath));
114985
- return createHash35("sha256").update(joined.sort().join(`
115166
+ return createHash36("sha256").update(joined.sort().join(`
114986
115167
  `)).digest("hex");
114987
115168
  }
114988
115169
  function writeStationHydration(options) {
@@ -115139,7 +115320,7 @@ var init_hydrate = __esm(() => {
115139
115320
  });
115140
115321
 
115141
115322
  // src/lib/prepare-skill.ts
115142
- import { createHash as createHash36, randomUUID as randomUUID12 } from "crypto";
115323
+ import { createHash as createHash37, randomUUID as randomUUID12 } from "crypto";
115143
115324
  import { chmodSync as chmodSync5, existsSync as existsSync37, lstatSync as lstatSync35, mkdirSync as mkdirSync29, mkdtempSync as mkdtempSync6, readFileSync as readFileSync44, readdirSync as readdirSync25, renameSync as renameSync14, rmSync as rmSync9, writeFileSync as writeFileSync30 } from "fs";
115144
115325
  import { tmpdir as tmpdir2 } from "os";
115145
115326
  import { dirname as dirname37, isAbsolute as isAbsolute31, join as join65, relative as relative12, resolve as resolve29, sep as sep15 } from "path";
@@ -115338,7 +115519,7 @@ function snapshot2(root2) {
115338
115519
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
115339
115520
  throw new Error("Skill root must be a real directory, not a symlink.");
115340
115521
  const files = [], directories = [];
115341
- const hash6 = createHash36("sha256").update(`${rootStat.dev}:${rootStat.ino}\x00`);
115522
+ const hash6 = createHash37("sha256").update(`${rootStat.dev}:${rootStat.ino}\x00`);
115342
115523
  let count = 0, bytes = 0;
115343
115524
  const walk2 = (relative13) => {
115344
115525
  for (const name of readdirSync25(join65(root2, relative13)).sort()) {
@@ -115769,7 +115950,7 @@ function createRegistrySyncArtifact(options = {}) {
115769
115950
  const includeDocs = options.includeDocs ?? true;
115770
115951
  const includeRequirements = options.includeRequirements ?? true;
115771
115952
  const includeValidation = options.includeValidation ?? true;
115772
- const packageName = options.packageName ?? "@hasna/skills";
115953
+ const packageName2 = options.packageName ?? "@hasna/skills";
115773
115954
  const packageVersion = options.packageVersion ?? package_default.version;
115774
115955
  const sourceRepository = options.sourceRepository ?? "hasna/skills";
115775
115956
  const registry2 = [...loadRegistryProfile(profile)].sort((a, b) => a.name.localeCompare(b.name));
@@ -115787,7 +115968,7 @@ function createRegistrySyncArtifact(options = {}) {
115787
115968
  tags: [...skill.tags].sort(),
115788
115969
  dependencies: [...skill.dependencies ?? []].sort(),
115789
115970
  source: {
115790
- packageName,
115971
+ packageName: packageName2,
115791
115972
  packageVersion,
115792
115973
  repository: sourceRepository,
115793
115974
  directory: directory2
@@ -115805,7 +115986,7 @@ function createRegistrySyncArtifact(options = {}) {
115805
115986
  return {
115806
115987
  schemaVersion: 1,
115807
115988
  source: {
115808
- packageName,
115989
+ packageName: packageName2,
115809
115990
  packageVersion,
115810
115991
  repository: sourceRepository,
115811
115992
  profile
@@ -115881,10 +116062,10 @@ async function exportSkillVersionForReview(spec, output2, client2) {
115881
116062
  const target = assertPrivateDestination(output2);
115882
116063
  if (existsSync38(target))
115883
116064
  throw new Error("Review output already exists; choose a new filename.");
115884
- const metadata = await client2.getSkillVersion(slug2, version2);
115885
- if (!metadata)
116065
+ const metadata2 = await client2.getSkillVersion(slug2, version2);
116066
+ if (!metadata2)
115886
116067
  throw new Error(`Published skill version ${slug2}@${version2} was not found.`);
115887
- if (!/^[a-f0-9]{64}$/i.test(metadata.bundleSha256) || !Number.isSafeInteger(metadata.bundleByteSize) || metadata.bundleByteSize < 1 || metadata.bundleByteSize > MAX_REVIEW_BUNDLE_BYTES) {
116068
+ if (!/^[a-f0-9]{64}$/i.test(metadata2.bundleSha256) || !Number.isSafeInteger(metadata2.bundleByteSize) || metadata2.bundleByteSize < 1 || metadata2.bundleByteSize > MAX_REVIEW_BUNDLE_BYTES) {
115888
116069
  throw new Error("Published bundle metadata is invalid or exceeds the review size limit.");
115889
116070
  }
115890
116071
  const response = await client2.getBundle(slug2, version2);
@@ -115896,12 +116077,12 @@ async function exportSkillVersionForReview(spec, output2, client2) {
115896
116077
  }
115897
116078
  const headerSha = response.headers.get("X-Skill-Bundle-Sha256");
115898
116079
  const headerVersion = response.headers.get("X-Skill-Version");
115899
- if (!headerSha || headerSha.toLowerCase() !== metadata.bundleSha256.toLowerCase() || headerVersion !== version2) {
116080
+ if (!headerSha || headerSha.toLowerCase() !== metadata2.bundleSha256.toLowerCase() || headerVersion !== version2) {
115900
116081
  response.body?.cancel().catch(() => {});
115901
116082
  throw new Error("Published bundle headers do not match the exact version metadata.");
115902
116083
  }
115903
- const bytes = await readBoundedResponse(response, Math.min(metadata.bundleByteSize, MAX_REVIEW_BUNDLE_BYTES));
115904
- if (bytes.byteLength !== metadata.bundleByteSize || sha256Hex(bytes) !== metadata.bundleSha256.toLowerCase()) {
116084
+ const bytes = await readBoundedResponse(response, Math.min(metadata2.bundleByteSize, MAX_REVIEW_BUNDLE_BYTES));
116085
+ if (bytes.byteLength !== metadata2.bundleByteSize || sha256Hex(bytes) !== metadata2.bundleSha256.toLowerCase()) {
115905
116086
  throw new Error("Published bundle bytes do not match the exact version metadata.");
115906
116087
  }
115907
116088
  const fd = openSync25(target, constants23.O_WRONLY | constants23.O_CREAT | constants23.O_EXCL | constants23.O_NOFOLLOW, 384);
@@ -115915,7 +116096,7 @@ async function exportSkillVersionForReview(spec, output2, client2) {
115915
116096
  } finally {
115916
116097
  closeSync25(fd);
115917
116098
  }
115918
- return { slug: slug2, version: version2, bundleSha256: metadata.bundleSha256.toLowerCase(), bundleByteSize: bytes.byteLength, output: target };
116099
+ return { slug: slug2, version: version2, bundleSha256: metadata2.bundleSha256.toLowerCase(), bundleByteSize: bytes.byteLength, output: target };
115919
116100
  }
115920
116101
  var MAX_REVIEW_BUNDLE_BYTES, SLUG2, NATIVE_CONTAINERS;
115921
116102
  var init_review_export = __esm(() => {
@@ -118099,10 +118280,10 @@ async function reconcileRegistry(options = {}) {
118099
118280
  throw new ReconcileRegistryError(`Registry listing failed: expected an array of skills, got ${shape}.`, ["Check HASNA_SKILLS_API_URL and the resolved credential; a failed listing must not be read as an empty registry."]);
118100
118281
  }
118101
118282
  const remotes = new Map;
118102
- for (const row of remoteRowsPayload) {
118103
- if (!row || typeof row !== "object")
118283
+ for (const row2 of remoteRowsPayload) {
118284
+ if (!row2 || typeof row2 !== "object")
118104
118285
  continue;
118105
- const skill = remoteRowToSkill(row);
118286
+ const skill = remoteRowToSkill(row2);
118106
118287
  if (skill)
118107
118288
  remotes.set(skill.slug, skill);
118108
118289
  }
package/bin/mcp.js CHANGED
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.10.39",
20022
+ version: "0.10.41",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.10.39",
10
+ version: "0.10.41",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.10.39",
24125
+ version: "0.10.41",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.10.39",
24128
+ version: "0.10.41",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -0,0 +1,17 @@
1
+ type Row = Record<string, unknown>;
2
+ export interface ReviewedRuntimeLockContext {
3
+ version: string;
4
+ archiveIntegrity: string;
5
+ packageManifest: Row;
6
+ registryOrigin: string;
7
+ minReleaseAge: number;
8
+ minReleaseAgeExclude: string[];
9
+ fetcher?: typeof fetch;
10
+ now?: number;
11
+ }
12
+ /** Validate the immutable install input without resolving any version ranges.
13
+ * npm ci subsequently verifies archive bytes and package/root range agreement;
14
+ * npm sbom forceActual validates the installed required closure, including optional peers.
15
+ */
16
+ export declare function validateReviewedRuntimeLock(bytes: Uint8Array, sha256: string, context: ReviewedRuntimeLockContext): Promise<void>;
17
+ export {};
@@ -28,6 +28,8 @@ export declare function updateCopyfileRuntime(version: string, options?: {
28
28
  fetcher?: typeof fetch;
29
29
  minReleaseAge?: number;
30
30
  minReleaseAgeExclude?: string[];
31
+ reviewedLock?: string;
32
+ reviewedLockSha256?: string;
31
33
  onLauncherSwitched?: (path: string) => void;
32
34
  }): Promise<JsonObject>;
33
35
  export declare function rollbackCopyfileRuntime(receiptId: string, options?: {
package/dist/index.js CHANGED
@@ -11040,7 +11040,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
11040
11040
  // package.json
11041
11041
  var package_default = {
11042
11042
  name: "@hasna/skills",
11043
- version: "0.10.39",
11043
+ version: "0.10.41",
11044
11044
  description: "Skills library for AI coding agents",
11045
11045
  type: "module",
11046
11046
  bin: {
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.10.39",
32376
+ version: "0.10.41",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.39",
3
+ "version": "0.10.41",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {