@hasna/skills 0.5.4 → 0.5.5
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 +72 -7
- package/bin/index.js +1875 -595
- package/bin/mcp.js +1780 -328
- package/bin/migrate.js +176 -4
- package/bin/server.js +300 -76
- package/bin/worker.js +294 -103
- package/dist/admin-contract.js +166 -168
- package/dist/cli/commands/private-publications.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1059 -410
- package/dist/lib/fleet-credentials.d.ts +6 -1
- package/dist/lib/home-adoption.d.ts +2 -7
- package/dist/lib/mcp-contracts.d.ts +1 -0
- package/dist/lib/private-publication-customer.d.ts +10 -0
- package/dist/lib/private-publication-recovery.d.ts +43 -0
- package/dist/lib/remote-account.d.ts +5 -0
- package/dist/lib/remote-auth.d.ts +3 -0
- package/dist/lib/remote-client.d.ts +2 -2
- package/dist/lib/remote-private-publications.d.ts +74 -0
- package/dist/mcp/private-publication-tools.d.ts +2 -0
- package/dist/sdk/execution/dispatchers/ecs.d.ts +25 -14
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +1137 -463
- package/dist/server/types.d.ts +2 -0
- package/dist/storage.js +33 -35
- package/package.json +1 -1
package/bin/migrate.js
CHANGED
|
@@ -7,7 +7,7 @@ import { join as join6 } from "path";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@hasna/skills",
|
|
10
|
-
version: "0.5.
|
|
10
|
+
version: "0.5.5",
|
|
11
11
|
description: "Skills library for AI coding agents",
|
|
12
12
|
type: "module",
|
|
13
13
|
bin: {
|
|
@@ -461,7 +461,40 @@ function publicPrincipal(partial = {}) {
|
|
|
461
461
|
// src/server/rows.ts
|
|
462
462
|
import { randomUUID } from "crypto";
|
|
463
463
|
|
|
464
|
+
// src/lib/skill-entry-path.ts
|
|
465
|
+
class SkillEntryPaths {
|
|
466
|
+
files = new Set;
|
|
467
|
+
directories = new Set;
|
|
468
|
+
add(path, maxBytes, invalid, limit) {
|
|
469
|
+
if (path.length > maxBytes)
|
|
470
|
+
limit();
|
|
471
|
+
const encoded = new TextEncoder().encode(path);
|
|
472
|
+
if (encoded.byteLength > maxBytes)
|
|
473
|
+
limit();
|
|
474
|
+
if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(encoded) !== path)
|
|
475
|
+
invalid("Invalid UTF-8 entry path");
|
|
476
|
+
if (!path || /[\\:\x00-\x1f\x7f]/u.test(path))
|
|
477
|
+
invalid("Unsafe entry path");
|
|
478
|
+
if (path.split("/").some((segment) => !segment || segment === "." || segment === ".."))
|
|
479
|
+
invalid("Unsafe entry path segment");
|
|
480
|
+
const key = path.normalize("NFC").toLowerCase().normalize("NFC");
|
|
481
|
+
if (this.files.has(key) || this.directories.has(key))
|
|
482
|
+
invalid("Duplicate or conflicting entry path");
|
|
483
|
+
const parents = key.split("/");
|
|
484
|
+
parents.pop();
|
|
485
|
+
while (parents.length) {
|
|
486
|
+
const parent = parents.join("/");
|
|
487
|
+
if (this.files.has(parent))
|
|
488
|
+
invalid("Conflicting entry file ancestor");
|
|
489
|
+
this.directories.add(parent);
|
|
490
|
+
parents.pop();
|
|
491
|
+
}
|
|
492
|
+
this.files.add(key);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
464
496
|
// src/lib/skill-bundle.ts
|
|
497
|
+
var BLOCK = 512;
|
|
465
498
|
var ANY_SEGMENT_EXCLUDES = new Set([
|
|
466
499
|
".git",
|
|
467
500
|
".ds_store",
|
|
@@ -598,6 +631,136 @@ var SKILL_BUNDLE_INSPECTION_LIMITS = Object.freeze({
|
|
|
598
631
|
timeoutMs: 5000
|
|
599
632
|
});
|
|
600
633
|
|
|
634
|
+
class SkillBundleInspectionError extends Error {
|
|
635
|
+
code;
|
|
636
|
+
constructor(code, message) {
|
|
637
|
+
super(message);
|
|
638
|
+
this.code = code;
|
|
639
|
+
this.name = "SkillBundleInspectionError";
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function invalidBundle(message) {
|
|
643
|
+
throw new SkillBundleInspectionError("BUNDLE_INVALID", message);
|
|
644
|
+
}
|
|
645
|
+
class BoundedTarReader {
|
|
646
|
+
limits;
|
|
647
|
+
check;
|
|
648
|
+
header = new Uint8Array(BLOCK);
|
|
649
|
+
headerOffset = 0;
|
|
650
|
+
pending;
|
|
651
|
+
bodyOffset = 0;
|
|
652
|
+
padding = 0;
|
|
653
|
+
zeroBlocks = 0;
|
|
654
|
+
entries = [];
|
|
655
|
+
paths = new SkillEntryPaths;
|
|
656
|
+
fileBytes = 0;
|
|
657
|
+
constructor(limits, check) {
|
|
658
|
+
this.limits = limits;
|
|
659
|
+
this.check = check;
|
|
660
|
+
}
|
|
661
|
+
push(chunk) {
|
|
662
|
+
let offset = 0;
|
|
663
|
+
while (offset < chunk.byteLength) {
|
|
664
|
+
this.check();
|
|
665
|
+
if (this.pending) {
|
|
666
|
+
const count = Math.min(this.pending.bytes.byteLength - this.bodyOffset, chunk.byteLength - offset);
|
|
667
|
+
this.pending.bytes.set(chunk.subarray(offset, offset + count), this.bodyOffset);
|
|
668
|
+
offset += count;
|
|
669
|
+
this.bodyOffset += count;
|
|
670
|
+
if (this.bodyOffset === this.pending.bytes.byteLength) {
|
|
671
|
+
this.entries.push(this.pending);
|
|
672
|
+
this.pending = undefined;
|
|
673
|
+
}
|
|
674
|
+
} else if (this.padding) {
|
|
675
|
+
const count = Math.min(this.padding, chunk.byteLength - offset);
|
|
676
|
+
if (chunk.subarray(offset, offset + count).some((byte) => byte !== 0))
|
|
677
|
+
invalidBundle("Nonzero tar body padding");
|
|
678
|
+
offset += count;
|
|
679
|
+
this.padding -= count;
|
|
680
|
+
} else {
|
|
681
|
+
const count = Math.min(BLOCK - this.headerOffset, chunk.byteLength - offset);
|
|
682
|
+
this.header.set(chunk.subarray(offset, offset + count), this.headerOffset);
|
|
683
|
+
offset += count;
|
|
684
|
+
this.headerOffset += count;
|
|
685
|
+
if (this.headerOffset === BLOCK) {
|
|
686
|
+
this.readHeader();
|
|
687
|
+
this.headerOffset = 0;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
finish() {
|
|
693
|
+
this.check();
|
|
694
|
+
if (this.pending || this.padding || this.headerOffset || this.zeroBlocks < 2)
|
|
695
|
+
invalidBundle("Truncated tar bundle");
|
|
696
|
+
return this.entries;
|
|
697
|
+
}
|
|
698
|
+
readHeader() {
|
|
699
|
+
this.check();
|
|
700
|
+
const h = this.header;
|
|
701
|
+
if (h.every((byte) => byte === 0)) {
|
|
702
|
+
this.zeroBlocks++;
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (this.zeroBlocks)
|
|
706
|
+
invalidBundle("Nonzero tar data after terminator");
|
|
707
|
+
if (this.entries.length >= this.limits.entries)
|
|
708
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle entry limit exceeded");
|
|
709
|
+
let checksum = 0;
|
|
710
|
+
for (let i = 0;i < BLOCK; i++)
|
|
711
|
+
checksum += i >= 148 && i < 156 ? 32 : h[i];
|
|
712
|
+
if (tarOctal(h.subarray(148, 156)) !== checksum)
|
|
713
|
+
invalidBundle("Invalid tar header checksum");
|
|
714
|
+
if (new TextDecoder().decode(h.subarray(257, 265)) !== "ustar\x00" + "00")
|
|
715
|
+
invalidBundle("Unsupported tar format");
|
|
716
|
+
if (h[156] !== 48 && h[156] !== 0 || h.subarray(157, 257).some((b) => b !== 0) || h.subarray(345).some((b) => b !== 0))
|
|
717
|
+
invalidBundle("Unsupported tar entry or path prefix");
|
|
718
|
+
const mode = tarOctal(h.subarray(100, 108));
|
|
719
|
+
if (mode > 511)
|
|
720
|
+
invalidBundle("Unsupported tar permission bits");
|
|
721
|
+
tarOctal(h.subarray(108, 116));
|
|
722
|
+
tarOctal(h.subarray(116, 124));
|
|
723
|
+
tarOctal(h.subarray(136, 148));
|
|
724
|
+
const size = tarOctal(h.subarray(124, 136));
|
|
725
|
+
if (size > this.limits.fileBytes || this.fileBytes + size > this.limits.decompressedBytes) {
|
|
726
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle file byte limit exceeded");
|
|
727
|
+
}
|
|
728
|
+
const name = h.subarray(0, 100);
|
|
729
|
+
const end = name.indexOf(0);
|
|
730
|
+
if (end !== -1 && name.subarray(end).some((b) => b !== 0))
|
|
731
|
+
invalidBundle("Invalid tar path padding");
|
|
732
|
+
const raw = end === -1 ? name : name.subarray(0, end);
|
|
733
|
+
if (raw.byteLength > this.limits.pathBytes)
|
|
734
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
|
|
735
|
+
let path;
|
|
736
|
+
try {
|
|
737
|
+
path = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
|
|
738
|
+
} catch {
|
|
739
|
+
return invalidBundle("Invalid UTF-8 bundle path");
|
|
740
|
+
}
|
|
741
|
+
this.paths.add(path, this.limits.pathBytes, invalidBundle, () => {
|
|
742
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
|
|
743
|
+
});
|
|
744
|
+
this.fileBytes += size;
|
|
745
|
+
this.pending = { path, mode, bytes: new Uint8Array(new ArrayBuffer(size)) };
|
|
746
|
+
this.bodyOffset = 0;
|
|
747
|
+
this.padding = (BLOCK - size % BLOCK) % BLOCK;
|
|
748
|
+
if (!size) {
|
|
749
|
+
this.entries.push(this.pending);
|
|
750
|
+
this.pending = undefined;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function tarOctal(field) {
|
|
755
|
+
const text = new TextDecoder().decode(field);
|
|
756
|
+
if (!/^[0-7]+[\0 ]*$/.test(text))
|
|
757
|
+
invalidBundle("Invalid tar octal field");
|
|
758
|
+
const value = Number.parseInt(text, 8);
|
|
759
|
+
if (!Number.isSafeInteger(value))
|
|
760
|
+
invalidBundle("Tar integer is out of range");
|
|
761
|
+
return value;
|
|
762
|
+
}
|
|
763
|
+
|
|
601
764
|
// src/server/rows.ts
|
|
602
765
|
function nowIso() {
|
|
603
766
|
return new Date().toISOString();
|
|
@@ -1083,10 +1246,16 @@ class SqliteSkillsStore {
|
|
|
1083
1246
|
const orgId = input.principal.orgId;
|
|
1084
1247
|
const now = nowIso();
|
|
1085
1248
|
return this.db.transaction(() => {
|
|
1086
|
-
const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
1249
|
+
const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at, source FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
1087
1250
|
const previousSha = typeof previous?.bundle_sha256 === "string" ? previous.bundle_sha256 : null;
|
|
1088
1251
|
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? previous.revision_id : null;
|
|
1089
1252
|
const tombstoned = previous?.tombstoned_at != null;
|
|
1253
|
+
if (input.seedBundledOnly && input.expectedRevisionId && !previous) {
|
|
1254
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
|
|
1255
|
+
}
|
|
1256
|
+
if (input.seedBundledOnly && previous && (tombstoned || previous.source !== "bundled")) {
|
|
1257
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
1258
|
+
}
|
|
1090
1259
|
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? previous.skill_md : null;
|
|
1091
1260
|
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
1092
1261
|
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
@@ -1160,8 +1329,9 @@ class SqliteSkillsStore {
|
|
|
1160
1329
|
tombstoned_at = NULL,
|
|
1161
1330
|
tombstone_purge_after = NULL,
|
|
1162
1331
|
updated_at = excluded.updated_at
|
|
1163
|
-
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
1164
|
-
|
|
1332
|
+
WHERE (skills_registry.tombstoned_at IS NOT NULL OR skills_registry.revision_id = ?)
|
|
1333
|
+
AND (? = 0 OR (skills_registry.tombstoned_at IS NULL AND skills_registry.source = 'bundled'
|
|
1334
|
+
AND skills_registry.revision_id = ?))
|
|
1165
1335
|
RETURNING *`, [
|
|
1166
1336
|
orgId,
|
|
1167
1337
|
input.slug,
|
|
@@ -1179,6 +1349,8 @@ class SqliteSkillsStore {
|
|
|
1179
1349
|
revisionId,
|
|
1180
1350
|
now,
|
|
1181
1351
|
now,
|
|
1352
|
+
input.expectedRevisionId ?? NO_REVISION_SENTINEL,
|
|
1353
|
+
input.seedBundledOnly ? 1 : 0,
|
|
1182
1354
|
input.expectedRevisionId ?? NO_REVISION_SENTINEL
|
|
1183
1355
|
]);
|
|
1184
1356
|
if (!row) {
|