@dot-skill/core 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pack.js ADDED
@@ -0,0 +1,191 @@
1
+ import { zipSync, unzipSync, strToU8, strFromU8 } from "fflate";
2
+ import { packageDigestFromContent, sha256Digest } from "./hash.js";
3
+ import { assertSafePaths, MAX_COMPRESSION_RATIO, MAX_ENTRIES, MAX_UNCOMPRESSED_BYTES, normalizePath, } from "./paths.js";
4
+ function toBytes(data) {
5
+ return typeof data === "string" ? strToU8(data) : data;
6
+ }
7
+ function textEncode(obj) {
8
+ return strToU8(JSON.stringify(obj, null, 2) + "\n");
9
+ }
10
+ export function buildFileMap(pkg) {
11
+ const files = {};
12
+ files["workflow.json"] = textEncode(pkg.workflow);
13
+ for (const item of pkg.knowledge) {
14
+ files[`knowledge/${item.id}.json`] = textEncode(item);
15
+ }
16
+ if (pkg.prompts) {
17
+ for (const [name, body] of Object.entries(pkg.prompts)) {
18
+ files[`prompts/${normalizePath(name)}`] = toBytes(body);
19
+ }
20
+ }
21
+ if (pkg.resources) {
22
+ for (const [name, body] of Object.entries(pkg.resources)) {
23
+ files[`resources/${normalizePath(name)}`] = toBytes(body);
24
+ }
25
+ }
26
+ if (pkg.artifacts) {
27
+ for (const [name, body] of Object.entries(pkg.artifacts)) {
28
+ files[`artifacts/${normalizePath(name)}`] = toBytes(body);
29
+ }
30
+ }
31
+ if (pkg.provenance?.recipe) {
32
+ files["provenance/recipe.json"] = textEncode(pkg.provenance.recipe);
33
+ }
34
+ if (pkg.provenance?.source) {
35
+ files["provenance/source.json"] = textEncode(pkg.provenance.source);
36
+ }
37
+ if (pkg.provenance?.journey) {
38
+ files["provenance/journey.json"] = textEncode(pkg.provenance.journey);
39
+ }
40
+ if (pkg.provenance?.generation_usage) {
41
+ files["provenance/generation_usage.json"] = textEncode(pkg.provenance.generation_usage);
42
+ }
43
+ if (pkg.provenance?.proof) {
44
+ files["provenance/proof.json"] = textEncode(pkg.provenance.proof);
45
+ }
46
+ if (pkg.provenance?.compilation_report) {
47
+ files["provenance/compilation_report.json"] = textEncode(pkg.provenance.compilation_report);
48
+ }
49
+ if (pkg.attestation && !pkg.signatures?.["creation.dsse.json"]) {
50
+ files["signatures/creation.attestation.json"] = textEncode(pkg.attestation);
51
+ }
52
+ if (pkg.signatures) {
53
+ for (const [name, body] of Object.entries(pkg.signatures)) {
54
+ files[`signatures/${normalizePath(name)}`] = textEncode(body);
55
+ }
56
+ }
57
+ if (pkg.anchors) {
58
+ pkg.anchors.forEach((anchor, i) => {
59
+ const path = `signatures/anchors/${i}-${anchor.kind}.json`;
60
+ if (!files[path])
61
+ files[path] = textEncode(anchor);
62
+ });
63
+ }
64
+ return files;
65
+ }
66
+ /**
67
+ * Content index covers every file except `skill.json` and `signatures/**`.
68
+ * `package_digest` is the digest of that index (RFC8785 JCS + SHA-256).
69
+ */
70
+ export function finalizeManifest(base, files) {
71
+ const content = Object.keys(files)
72
+ .filter((p) => p !== "skill.json" && !p.startsWith("signatures/"))
73
+ .sort()
74
+ .map((path) => ({
75
+ path,
76
+ digest: sha256Digest(files[path]),
77
+ bytes: files[path].byteLength,
78
+ }));
79
+ return {
80
+ ...base,
81
+ mint: base.mint ?? { mint_status: "draft" },
82
+ content,
83
+ package_digest: packageDigestFromContent(content),
84
+ };
85
+ }
86
+ export function packSkill(pkg, _opts = {}) {
87
+ const files = buildFileMap(pkg);
88
+ const manifest = finalizeManifest(pkg.manifest, files);
89
+ files["skill.json"] = textEncode(manifest);
90
+ assertSafePaths(Object.keys(files));
91
+ if (Object.keys(files).length > MAX_ENTRIES) {
92
+ throw new Error(`Too many entries: ${Object.keys(files).length}`);
93
+ }
94
+ let total = 0;
95
+ for (const bytes of Object.values(files))
96
+ total += bytes.byteLength;
97
+ if (total > MAX_UNCOMPRESSED_BYTES) {
98
+ throw new Error(`Package too large: ${total} bytes`);
99
+ }
100
+ return zipSync(files, { level: 6 });
101
+ }
102
+ export function unpackSkill(archive) {
103
+ if (archive.byteLength > MAX_UNCOMPRESSED_BYTES * 2) {
104
+ throw new Error("Archive too large to unpack");
105
+ }
106
+ const unzipped = unzipSync(archive);
107
+ const paths = Object.keys(unzipped);
108
+ if (paths.length > MAX_ENTRIES)
109
+ throw new Error("Too many zip entries");
110
+ assertSafePaths(paths);
111
+ let uncompressed = 0;
112
+ for (const data of Object.values(unzipped)) {
113
+ uncompressed += data.byteLength;
114
+ if (uncompressed > MAX_UNCOMPRESSED_BYTES) {
115
+ throw new Error("Uncompressed size exceeds limit");
116
+ }
117
+ const ratio = archive.byteLength > 0 ? uncompressed / archive.byteLength : 0;
118
+ if (ratio > MAX_COMPRESSION_RATIO && uncompressed > 1_000_000) {
119
+ throw new Error("Suspicious compression ratio");
120
+ }
121
+ }
122
+ const skillJson = unzipped["skill.json"];
123
+ if (!skillJson)
124
+ throw new Error("Missing skill.json");
125
+ const workflowJson = unzipped["workflow.json"];
126
+ if (!workflowJson)
127
+ throw new Error("Missing workflow.json");
128
+ const manifest = JSON.parse(strFromU8(skillJson));
129
+ const workflow = JSON.parse(strFromU8(workflowJson));
130
+ const knowledge = [];
131
+ for (const [path, data] of Object.entries(unzipped)) {
132
+ if (path.startsWith("knowledge/") && path.endsWith(".json")) {
133
+ knowledge.push(JSON.parse(strFromU8(data)));
134
+ }
135
+ }
136
+ let compilation_report;
137
+ if (unzipped["provenance/compilation_report.json"]) {
138
+ compilation_report = JSON.parse(strFromU8(unzipped["provenance/compilation_report.json"]));
139
+ }
140
+ const prompts = {};
141
+ const resources = {};
142
+ const artifacts = {};
143
+ const signatures = {};
144
+ for (const [path, data] of Object.entries(unzipped)) {
145
+ if (path.startsWith("prompts/"))
146
+ prompts[path.slice("prompts/".length)] = strFromU8(data);
147
+ if (path.startsWith("resources/"))
148
+ resources[path.slice("resources/".length)] = data;
149
+ if (path.startsWith("artifacts/"))
150
+ artifacts[path.slice("artifacts/".length)] = data;
151
+ if (path.startsWith("signatures/") && path.endsWith(".json")) {
152
+ signatures[path.slice("signatures/".length)] = JSON.parse(strFromU8(data));
153
+ }
154
+ }
155
+ const creation = signatures["creation.dsse.json"];
156
+ const attestation = creation?.attestation ??
157
+ signatures["creation.attestation.json"];
158
+ const anchorsFromSig = Object.entries(signatures)
159
+ .filter(([k]) => k.startsWith("anchors/"))
160
+ .map(([, v]) => v);
161
+ const raw = {
162
+ manifest,
163
+ workflow,
164
+ knowledge,
165
+ prompts,
166
+ artifacts,
167
+ resources,
168
+ provenance: {
169
+ recipe: unzipped["provenance/recipe.json"]
170
+ ? JSON.parse(strFromU8(unzipped["provenance/recipe.json"]))
171
+ : undefined,
172
+ source: unzipped["provenance/source.json"]
173
+ ? JSON.parse(strFromU8(unzipped["provenance/source.json"]))
174
+ : undefined,
175
+ journey: unzipped["provenance/journey.json"]
176
+ ? JSON.parse(strFromU8(unzipped["provenance/journey.json"]))
177
+ : undefined,
178
+ generation_usage: unzipped["provenance/generation_usage.json"]
179
+ ? JSON.parse(strFromU8(unzipped["provenance/generation_usage.json"]))
180
+ : undefined,
181
+ proof: unzipped["provenance/proof.json"]
182
+ ? JSON.parse(strFromU8(unzipped["provenance/proof.json"]))
183
+ : undefined,
184
+ compilation_report,
185
+ },
186
+ signatures,
187
+ attestation,
188
+ anchors: manifest.anchors?.length ? manifest.anchors : anchorsFromSig,
189
+ };
190
+ return { files: unzipped, manifest, workflow, knowledge, compilation_report, raw };
191
+ }
@@ -0,0 +1,5 @@
1
+ export declare function normalizePath(path: string): string;
2
+ export declare function assertSafePaths(paths: string[]): void;
3
+ export declare const MAX_ENTRIES = 10000;
4
+ export declare const MAX_UNCOMPRESSED_BYTES: number;
5
+ export declare const MAX_COMPRESSION_RATIO = 100;
package/dist/paths.js ADDED
@@ -0,0 +1,22 @@
1
+ const UNSAFE = /(^\/)|(\.\.)|(^$)/;
2
+ export function normalizePath(path) {
3
+ const nfc = path.normalize("NFC").replace(/\\/g, "/");
4
+ if (nfc.includes("\0"))
5
+ throw new Error(`Null byte in path: ${path}`);
6
+ if (UNSAFE.test(nfc) || nfc.split("/").some((s) => s === "" || s === "." || s === "..")) {
7
+ throw new Error(`Unsafe path: ${path}`);
8
+ }
9
+ return nfc;
10
+ }
11
+ export function assertSafePaths(paths) {
12
+ const seen = new Set();
13
+ for (const p of paths) {
14
+ const n = normalizePath(p);
15
+ if (seen.has(n))
16
+ throw new Error(`Duplicate path: ${n}`);
17
+ seen.add(n);
18
+ }
19
+ }
20
+ export const MAX_ENTRIES = 10_000;
21
+ export const MAX_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;
22
+ export const MAX_COMPRESSION_RATIO = 100;
@@ -0,0 +1,32 @@
1
+ import type { SkillManifest, Workflow } from "@dot-skill/protocol";
2
+ export interface ValidationIssue {
3
+ severity: "error" | "warning";
4
+ code: string;
5
+ message: string;
6
+ path?: string;
7
+ }
8
+ export interface ValidationResult {
9
+ ok: boolean;
10
+ issues: ValidationIssue[];
11
+ manifest?: SkillManifest;
12
+ workflow?: Workflow;
13
+ }
14
+ export declare function validateManifestShape(manifest: SkillManifest): ValidationIssue[];
15
+ export declare function validateWorkflowShape(workflow: Workflow, entrypoint: string): ValidationIssue[];
16
+ export declare function validatePackageBytes(archive: Uint8Array): ValidationResult;
17
+ export declare function inspectSkill(archive: Uint8Array): {
18
+ ok: boolean;
19
+ summary: {
20
+ id: string;
21
+ version: string;
22
+ title: string;
23
+ description: string;
24
+ inputs: string[];
25
+ permissions: string[];
26
+ capabilities: string[];
27
+ package_digest: string;
28
+ mint_status?: string;
29
+ needs_human_review?: boolean;
30
+ };
31
+ issues: ValidationIssue[];
32
+ };
@@ -0,0 +1,215 @@
1
+ import { CONTAINER_VERSION, PROTOCOL_VERSION, WORKFLOW_DIALECT_VERSION, } from "@dot-skill/protocol";
2
+ import { packageDigestFromContent, sha256Digest } from "./hash.js";
3
+ import { unpackSkill } from "./pack.js";
4
+ export function validateManifestShape(manifest) {
5
+ const issues = [];
6
+ if (manifest.kind !== "dot-skill") {
7
+ issues.push({ severity: "error", code: "kind", message: "kind must be dot-skill" });
8
+ }
9
+ if (!manifest.id)
10
+ issues.push({ severity: "error", code: "id", message: "id required" });
11
+ if (!manifest.version)
12
+ issues.push({ severity: "error", code: "version", message: "version required" });
13
+ if (!manifest.title)
14
+ issues.push({ severity: "error", code: "title", message: "title required" });
15
+ if (!manifest.description)
16
+ issues.push({
17
+ severity: "error",
18
+ code: "description",
19
+ message: "description required",
20
+ });
21
+ if (!manifest.entrypoint)
22
+ issues.push({
23
+ severity: "error",
24
+ code: "entrypoint",
25
+ message: "entrypoint required",
26
+ });
27
+ if (manifest.container_version !== CONTAINER_VERSION) {
28
+ issues.push({
29
+ severity: "warning",
30
+ code: "container_version",
31
+ message: `Unexpected container_version ${manifest.container_version}`,
32
+ });
33
+ }
34
+ if (manifest.protocol_version !== PROTOCOL_VERSION) {
35
+ issues.push({
36
+ severity: "error",
37
+ code: "protocol_version",
38
+ message: `Unsupported protocol_version ${manifest.protocol_version}; expected ${PROTOCOL_VERSION}`,
39
+ });
40
+ }
41
+ if (manifest.mint?.mint_status === "minted") {
42
+ if (manifest.compile_profile !== "release") {
43
+ issues.push({
44
+ severity: "error",
45
+ code: "minted_profile",
46
+ message: "Minted packages must use compile_profile=release",
47
+ });
48
+ }
49
+ if (!manifest.completeness?.complete) {
50
+ issues.push({
51
+ severity: "error",
52
+ code: "minted_incomplete",
53
+ message: "Minted packages require a complete release report",
54
+ });
55
+ }
56
+ }
57
+ for (const input of manifest.inputs ?? []) {
58
+ if (input.sensitivity === "secret" && input.examples?.length) {
59
+ issues.push({
60
+ severity: "error",
61
+ code: "secret_examples",
62
+ message: `Input ${input.name} is secret but includes examples`,
63
+ path: input.name,
64
+ });
65
+ }
66
+ }
67
+ return issues;
68
+ }
69
+ export function validateWorkflowShape(workflow, entrypoint) {
70
+ const issues = [];
71
+ if (workflow.kind !== "workflow") {
72
+ issues.push({ severity: "error", code: "workflow_kind", message: "kind must be workflow" });
73
+ }
74
+ if (workflow.dialect_version !== WORKFLOW_DIALECT_VERSION) {
75
+ issues.push({
76
+ severity: "warning",
77
+ code: "dialect",
78
+ message: `Unexpected dialect_version ${workflow.dialect_version}`,
79
+ });
80
+ }
81
+ const ids = new Set(workflow.steps.map((s) => s.id));
82
+ if (!ids.has(entrypoint)) {
83
+ issues.push({
84
+ severity: "error",
85
+ code: "entrypoint_missing",
86
+ message: `Entrypoint ${entrypoint} not in steps`,
87
+ });
88
+ }
89
+ for (const step of workflow.steps) {
90
+ if (!step.id || !step.kind) {
91
+ issues.push({
92
+ severity: "error",
93
+ code: "step",
94
+ message: "Each step needs id and kind",
95
+ });
96
+ }
97
+ }
98
+ return issues;
99
+ }
100
+ export function validatePackageBytes(archive) {
101
+ const issues = [];
102
+ let unpacked;
103
+ try {
104
+ unpacked = unpackSkill(archive);
105
+ }
106
+ catch (e) {
107
+ return {
108
+ ok: false,
109
+ issues: [
110
+ {
111
+ severity: "error",
112
+ code: "unpack",
113
+ message: e instanceof Error ? e.message : String(e),
114
+ },
115
+ ],
116
+ };
117
+ }
118
+ issues.push(...validateManifestShape(unpacked.manifest));
119
+ issues.push(...validateWorkflowShape(unpacked.workflow, unpacked.manifest.entrypoint));
120
+ const computed = [];
121
+ for (const [path, data] of Object.entries(unpacked.files)) {
122
+ if (path === "skill.json" || path.startsWith("signatures/"))
123
+ continue;
124
+ const digest = sha256Digest(data);
125
+ computed.push({ path, digest });
126
+ const listed = unpacked.manifest.content.find((c) => c.path === path);
127
+ if (!listed) {
128
+ issues.push({
129
+ severity: "error",
130
+ code: "missing_content_entry",
131
+ message: `File ${path} not listed in manifest.content`,
132
+ path,
133
+ });
134
+ }
135
+ else if (listed.digest !== digest) {
136
+ issues.push({
137
+ severity: "error",
138
+ code: "digest_mismatch",
139
+ message: `Digest mismatch for ${path}`,
140
+ path,
141
+ });
142
+ }
143
+ }
144
+ for (const entry of unpacked.manifest.content) {
145
+ if (!unpacked.files[entry.path]) {
146
+ issues.push({
147
+ severity: "error",
148
+ code: "missing_file",
149
+ message: `Manifest lists missing file ${entry.path}`,
150
+ path: entry.path,
151
+ });
152
+ }
153
+ }
154
+ const expectedPkg = packageDigestFromContent(computed);
155
+ if (unpacked.manifest.package_digest !== expectedPkg) {
156
+ issues.push({
157
+ severity: "error",
158
+ code: "package_digest",
159
+ message: "package_digest does not match content index",
160
+ });
161
+ }
162
+ if (unpacked.manifest.policy.require_signatures) {
163
+ const sigs = Object.keys(unpacked.files).filter((p) => p.startsWith("signatures/"));
164
+ if (sigs.length === 0) {
165
+ issues.push({
166
+ severity: "error",
167
+ code: "signatures_required",
168
+ message: "Policy requires signatures but none present",
169
+ });
170
+ }
171
+ }
172
+ const ok = !issues.some((i) => i.severity === "error");
173
+ return {
174
+ ok,
175
+ issues,
176
+ manifest: unpacked.manifest,
177
+ workflow: unpacked.workflow,
178
+ };
179
+ }
180
+ export function inspectSkill(archive) {
181
+ const result = validatePackageBytes(archive);
182
+ if (!result.manifest) {
183
+ return {
184
+ ok: false,
185
+ summary: {
186
+ id: "",
187
+ version: "",
188
+ title: "",
189
+ description: "",
190
+ inputs: [],
191
+ permissions: [],
192
+ capabilities: [],
193
+ package_digest: "",
194
+ },
195
+ issues: result.issues,
196
+ };
197
+ }
198
+ const m = result.manifest;
199
+ return {
200
+ ok: result.ok,
201
+ summary: {
202
+ id: m.id,
203
+ version: m.version,
204
+ title: m.title,
205
+ description: m.description,
206
+ inputs: m.inputs.filter((i) => i.required).map((i) => i.name),
207
+ permissions: m.permissions.map((p) => p.side_effect_class),
208
+ capabilities: m.capabilities.map((c) => c.name),
209
+ package_digest: m.package_digest,
210
+ mint_status: m.mint?.mint_status ?? "draft",
211
+ needs_human_review: m.needs_human_review,
212
+ },
213
+ issues: result.issues,
214
+ };
215
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@dot-skill/core",
3
+ "version": "0.4.1",
4
+ "description": "Open .skill Protocol — pack, validate, compile, mint",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Bharat Dudeja",
8
+ "url": "https://github.com/bharatdudeja13-cmd"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/dot-skill/dot-skill.git",
19
+ "directory": "packages/core"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/index.js",
27
+ "types": "./dist/index.d.ts"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsc",
32
+ "prepack": "npm run build",
33
+ "clean": "rm -rf dist"
34
+ },
35
+ "dependencies": {
36
+ "@dot-skill/protocol": "^0.4.1",
37
+ "fflate": "^0.8.2"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^26.1.1",
41
+ "typescript": "^5.4.5"
42
+ }
43
+ }