@knolo/cli 6.2.0 → 6.2.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/README.md +46 -0
- package/bin/flight.mjs +367 -0
- package/bin/knolo.mjs +29 -0
- package/bin/skill.mjs +698 -0
- package/package.json +2 -2
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/closure.rs +395 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/cover.rs +692 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/evaluate.rs +343 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/json.rs +486 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/mod.rs +14 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/model.rs +1068 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/session.rs +1156 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/lib.rs +7 -1
package/bin/skill.mjs
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knolo skill
|
|
3
|
+
*
|
|
4
|
+
* Creates a Knolo skill pack: guidance plus an optional pinned V5 image.
|
|
5
|
+
* The pack names no host and no home directory. A directory catalog stores
|
|
6
|
+
* packs. It does not run a model and it does not answer questions.
|
|
7
|
+
* SATISFIED means evidence requirements were met.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { lstat, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
export const SKILL_PACK_FORMAT = 'knolo-skill-pack-v1';
|
|
17
|
+
export const MAX_KNOWLEDGE_FILES = 100;
|
|
18
|
+
export const MAX_KNOWLEDGE_BYTES = 1024 * 1024;
|
|
19
|
+
const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
20
|
+
const EXTENSIONS = new Set(['.md', '.txt', '.json']);
|
|
21
|
+
|
|
22
|
+
const USAGE = `Usage: knolo skill <init|check|pack|publish|install> [options]
|
|
23
|
+
|
|
24
|
+
knolo skill init <name> [--dir <parent>]
|
|
25
|
+
knolo skill check [<dir>] [--query <text>] [--json]
|
|
26
|
+
knolo skill pack [<dir>] [--out <dir>] [--json]
|
|
27
|
+
knolo skill publish <pack> --hub <dir> [--json]
|
|
28
|
+
knolo skill install <name> --hub <dir> --dir <path> [--json]
|
|
29
|
+
knolo skill install --pack <pack> --dir <path> [--json]
|
|
30
|
+
|
|
31
|
+
A skill is guidance for the host agent. Knolo does not call a model.
|
|
32
|
+
check searches the skill's own knowledge and returns a receipt id.
|
|
33
|
+
A receipt is an integrity check. It does not mean a claim is proven.
|
|
34
|
+
SATISFIED means the evidence requirements were met.
|
|
35
|
+
publish copies a pack into a directory catalog. install checks the pin
|
|
36
|
+
before it writes, and it writes only into --dir.
|
|
37
|
+
The catalog stores packs. It does not answer questions.
|
|
38
|
+
This catalog is not https://hub.knolo.dev.`;
|
|
39
|
+
|
|
40
|
+
export async function runSkillCli(argv) {
|
|
41
|
+
const { positionals, flags } = parseArgs(argv);
|
|
42
|
+
const sub = positionals[0] ?? '';
|
|
43
|
+
if (!sub || sub === 'help' || flags.help) {
|
|
44
|
+
console.log(USAGE);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (sub === 'init') return initSkill(positionals[1], flags);
|
|
48
|
+
if (sub === 'check') return checkSkill(positionals[1], flags);
|
|
49
|
+
if (sub === 'pack') return packSkill(positionals[1], flags);
|
|
50
|
+
if (sub === 'publish') return publishSkill(positionals[1], flags);
|
|
51
|
+
if (sub === 'install') return installSkill(positionals[1], flags);
|
|
52
|
+
throw new Error(`Unknown skill command "${sub}".\n${USAGE}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function skillNameError(name) {
|
|
56
|
+
if (typeof name !== 'string' || !NAME.test(name)) {
|
|
57
|
+
return 'SKILL_NAME: use lowercase letters, digits, and single hyphens.';
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ABSOLUTE_PATH =
|
|
63
|
+
/(?:^|[\s`"'(])(?:~\/|\/(?:home|Users|opt|var|tmp)\/|[A-Za-z]:\\)/u;
|
|
64
|
+
|
|
65
|
+
function hostLayoutError(text) {
|
|
66
|
+
if (ABSOLUTE_PATH.test(text)) {
|
|
67
|
+
return 'SKILL_GUIDANCE: a skill pack cannot name a home directory or an absolute path.';
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function guidanceError(body) {
|
|
73
|
+
const layout = hostLayoutError(body);
|
|
74
|
+
if (layout) return layout;
|
|
75
|
+
const patterns = [
|
|
76
|
+
/\b(?:skip|ignore|omit|bypass)\b[^.\n]{0,48}\b(?:pin|receipt)\b/giu,
|
|
77
|
+
/\bwithout (?:a |the )?(?:pin|receipt)\b/giu,
|
|
78
|
+
/\b(?:do not|don't|never) (?:cite|include|return)\b[^.\n]{0,48}\breceipt\b/giu,
|
|
79
|
+
];
|
|
80
|
+
for (const pattern of patterns) pattern.lastIndex = 0;
|
|
81
|
+
for (const pattern of patterns) {
|
|
82
|
+
for (const match of body.matchAll(pattern)) {
|
|
83
|
+
const start = Math.max(0, match.index - 20);
|
|
84
|
+
const prefix = body.slice(start, match.index).toLowerCase();
|
|
85
|
+
const negated = /\b(?:not|never)\s*$/u.test(prefix);
|
|
86
|
+
const tellsYouToDropTheReceipt = /\b(?:cite|include|return)\b/iu.test(
|
|
87
|
+
match[0]
|
|
88
|
+
);
|
|
89
|
+
if (negated && !tellsYouToDropTheReceipt) continue;
|
|
90
|
+
return `SKILL_GUIDANCE: the skill tells the agent to skip a pin or a receipt (${match[0]}).`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!/\breceipt\b/iu.test(body)) {
|
|
94
|
+
return 'SKILL_GUIDANCE: the skill must tell the agent to cite a receipt.';
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
!/\bintegrity\b/iu.test(body) &&
|
|
98
|
+
!/does not mean a claim is proven/iu.test(body)
|
|
99
|
+
) {
|
|
100
|
+
return 'SKILL_GUIDANCE: the skill must say that a receipt is an integrity check.';
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function initSkill(name, flags) {
|
|
106
|
+
const nameError = skillNameError(name);
|
|
107
|
+
if (nameError) throw new Error(nameError);
|
|
108
|
+
const parent = path.resolve(process.cwd(), flags.dir ?? '.');
|
|
109
|
+
const dir = path.join(parent, name);
|
|
110
|
+
return prepareSkillDir(dir, name, flags);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function prepareSkillDir(dir, name, flags) {
|
|
114
|
+
try {
|
|
115
|
+
await lstat(dir);
|
|
116
|
+
throw new Error(`SKILL_NAME: ${dir} already exists.`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
119
|
+
}
|
|
120
|
+
await mkdir(path.join(dir, 'knowledge'), { recursive: true });
|
|
121
|
+
const skill = skillTemplate(name);
|
|
122
|
+
await writeFile(path.join(dir, 'SKILL.md'), skill, { mode: 0o644 });
|
|
123
|
+
const summary = { ok: true, name, dir };
|
|
124
|
+
if (flags.json) console.log(JSON.stringify(summary));
|
|
125
|
+
else console.log(`wrote ${dir}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function skillTemplate(name) {
|
|
129
|
+
return `---
|
|
130
|
+
name: ${name}
|
|
131
|
+
description: Cite the pinned Knolo image for ${name} before answering.
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# ${name}
|
|
135
|
+
|
|
136
|
+
The host owns the model. This skill does not call one.
|
|
137
|
+
|
|
138
|
+
1. Search the pinned image for this skill.
|
|
139
|
+
2. Include the receipt id in the reply.
|
|
140
|
+
3. A receipt is an integrity check. It does not mean a claim is proven. SATISFIED means the evidence requirements were met.
|
|
141
|
+
4. Do not skip the pin. Do not invent a passage.
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function checkSkill(dirArg, flags) {
|
|
146
|
+
const loaded = await loadSkillDir(dirArg);
|
|
147
|
+
const report = await inspectSkill(loaded, flags.query);
|
|
148
|
+
if (flags.json) console.log(JSON.stringify(report));
|
|
149
|
+
else {
|
|
150
|
+
console.log(`skill ${report.name}`);
|
|
151
|
+
console.log(`receipt ${report.receiptId ?? 'none'}`);
|
|
152
|
+
console.log(`hits ${report.hits}`);
|
|
153
|
+
if (report.stateRoot) console.log(`state root ${report.stateRoot}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function packSkill(dirArg, flags) {
|
|
158
|
+
const loaded = await loadSkillDir(dirArg);
|
|
159
|
+
const built = await buildSkill(loaded, flags.query);
|
|
160
|
+
const out = path.resolve(
|
|
161
|
+
process.cwd(),
|
|
162
|
+
flags.out ??
|
|
163
|
+
path.join(loaded.dir, 'dist', `${loaded.parsed.name}.knolo-skill`)
|
|
164
|
+
);
|
|
165
|
+
try {
|
|
166
|
+
await lstat(out);
|
|
167
|
+
throw new Error(`SKILL_PACK: refusing to overwrite ${out}.`);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
170
|
+
}
|
|
171
|
+
await writePack(out, built);
|
|
172
|
+
const summary = packSummary(built, out);
|
|
173
|
+
if (flags.json) console.log(JSON.stringify(summary));
|
|
174
|
+
else {
|
|
175
|
+
console.log(`skill ${summary.name}`);
|
|
176
|
+
console.log(`wrote ${out}`);
|
|
177
|
+
console.log(`skill sha256 ${summary.skillSha256}`);
|
|
178
|
+
if (summary.image) {
|
|
179
|
+
console.log(`image sha256 ${summary.image.sha256}`);
|
|
180
|
+
console.log(`state root ${summary.image.stateRoot}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function publishSkill(packArg, flags) {
|
|
186
|
+
if (!flags.hub) throw new Error('Missing --hub');
|
|
187
|
+
const packDir = path.resolve(process.cwd(), packArg ?? '');
|
|
188
|
+
if (!packArg) throw new Error('SKILL_HUB: pass the pack directory.');
|
|
189
|
+
const pack = await readVerifiedPack(packDir);
|
|
190
|
+
const hub = path.resolve(process.cwd(), flags.hub);
|
|
191
|
+
const dest = path.join(hub, pack.manifest.name);
|
|
192
|
+
await rejectHostPath(dest);
|
|
193
|
+
try {
|
|
194
|
+
const existing = await readVerifiedPack(dest);
|
|
195
|
+
if (
|
|
196
|
+
existing.manifest.skillSha256 !== pack.manifest.skillSha256 ||
|
|
197
|
+
!sameImage(existing.manifest.image, pack.manifest.image)
|
|
198
|
+
) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`SKILL_HUB: ${pack.manifest.name} is already in the catalog with a different pin.`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const summary = {
|
|
204
|
+
ok: true,
|
|
205
|
+
name: pack.manifest.name,
|
|
206
|
+
hub: dest,
|
|
207
|
+
unchanged: true,
|
|
208
|
+
};
|
|
209
|
+
if (flags.json) console.log(JSON.stringify(summary));
|
|
210
|
+
else console.log(`unchanged ${dest}`);
|
|
211
|
+
return;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (!(error && error.code === 'ENOENT')) throw error;
|
|
214
|
+
}
|
|
215
|
+
await writePackFiles(dest, pack);
|
|
216
|
+
const summary = {
|
|
217
|
+
ok: true,
|
|
218
|
+
name: pack.manifest.name,
|
|
219
|
+
hub: dest,
|
|
220
|
+
unchanged: false,
|
|
221
|
+
};
|
|
222
|
+
if (flags.json) console.log(JSON.stringify(summary));
|
|
223
|
+
else console.log(`published ${dest}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function installSkill(nameArg, flags) {
|
|
227
|
+
if (!flags.dir) throw new Error('Missing --dir');
|
|
228
|
+
const destRoot = path.resolve(process.cwd(), flags.dir);
|
|
229
|
+
let pack;
|
|
230
|
+
if (flags.pack) {
|
|
231
|
+
pack = await readVerifiedPack(path.resolve(process.cwd(), flags.pack));
|
|
232
|
+
} else {
|
|
233
|
+
if (!nameArg) throw new Error('SKILL_HUB: pass a skill name or --pack.');
|
|
234
|
+
if (!flags.hub) throw new Error('Missing --hub');
|
|
235
|
+
const nameError = skillNameError(nameArg);
|
|
236
|
+
if (nameError) throw new Error(nameError);
|
|
237
|
+
pack = await readVerifiedPack(
|
|
238
|
+
path.join(path.resolve(process.cwd(), flags.hub), nameArg)
|
|
239
|
+
);
|
|
240
|
+
if (pack.manifest.name !== nameArg) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`SKILL_HUB: catalog entry ${nameArg} is a pack named ${pack.manifest.name}.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const dest = path.join(destRoot, pack.manifest.name);
|
|
247
|
+
await rejectHostPath(dest);
|
|
248
|
+
try {
|
|
249
|
+
await lstat(dest);
|
|
250
|
+
throw new Error(`SKILL_PACK: refusing to overwrite ${dest}.`);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
253
|
+
}
|
|
254
|
+
await writePackFiles(dest, pack);
|
|
255
|
+
const summary = { ok: true, name: pack.manifest.name, dir: dest };
|
|
256
|
+
if (flags.json) console.log(JSON.stringify(summary));
|
|
257
|
+
else console.log(`installed ${dest}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function loadSkillDir(dirArg) {
|
|
261
|
+
const dir = path.resolve(process.cwd(), dirArg ?? '.');
|
|
262
|
+
const raw = await readFile(path.join(dir, 'SKILL.md'));
|
|
263
|
+
const parsed = parseSkill(raw.toString('utf8'));
|
|
264
|
+
if (path.basename(dir) !== parsed.name) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`SKILL_NAME: directory ${path.basename(dir)} does not match frontmatter name ${parsed.name}.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return { dir, raw, parsed };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseSkill(text) {
|
|
273
|
+
if (!text.startsWith('---\n')) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
'SKILL_FRONTMATTER: SKILL.md must start with a frontmatter block.'
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const end = text.indexOf('\n---\n', 4);
|
|
279
|
+
if (end < 0)
|
|
280
|
+
throw new Error('SKILL_FRONTMATTER: SKILL.md frontmatter does not close.');
|
|
281
|
+
const fields = {};
|
|
282
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
283
|
+
if (!line.trim()) continue;
|
|
284
|
+
const match = /^([a-z]+):[ ]*(.*)$/u.exec(line);
|
|
285
|
+
if (!match) throw new Error(`SKILL_FRONTMATTER: cannot read "${line}".`);
|
|
286
|
+
fields[match[1]] = match[2].trim();
|
|
287
|
+
}
|
|
288
|
+
const keys = Object.keys(fields).sort();
|
|
289
|
+
if (keys.join(',') !== 'description,name') {
|
|
290
|
+
throw new Error('SKILL_FRONTMATTER: use only name and description.');
|
|
291
|
+
}
|
|
292
|
+
const nameError = skillNameError(fields.name);
|
|
293
|
+
if (nameError) throw new Error(nameError);
|
|
294
|
+
if (!fields.description)
|
|
295
|
+
throw new Error('SKILL_FRONTMATTER: description is empty.');
|
|
296
|
+
const described = hostLayoutError(fields.description);
|
|
297
|
+
if (described) throw new Error(described);
|
|
298
|
+
const body = text.slice(end + '\n---\n'.length);
|
|
299
|
+
const guidance = guidanceError(body);
|
|
300
|
+
if (guidance) throw new Error(guidance);
|
|
301
|
+
return { name: fields.name, description: fields.description, body };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function inspectSkill(loaded, queryFlag) {
|
|
305
|
+
const files = await listKnowledge(loaded.dir);
|
|
306
|
+
if (files.length === 0) {
|
|
307
|
+
return {
|
|
308
|
+
ok: true,
|
|
309
|
+
name: loaded.parsed.name,
|
|
310
|
+
receiptId: null,
|
|
311
|
+
hits: 0,
|
|
312
|
+
stateRoot: null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const built = await buildFromFiles(loaded, files, queryFlag);
|
|
316
|
+
if (built.hits < 1) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
'SKILL_CHECK: the knowledge search returned no hits. Pass --query with words from the source.'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
name: loaded.parsed.name,
|
|
324
|
+
receiptId: built.receiptId,
|
|
325
|
+
hits: built.hits,
|
|
326
|
+
stateRoot: built.image.stateRoot,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function buildSkill(loaded, queryFlag) {
|
|
331
|
+
const files = await listKnowledge(loaded.dir);
|
|
332
|
+
return buildFromFiles(loaded, files, queryFlag);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function buildFromFiles(loaded, files, queryFlag) {
|
|
336
|
+
const policy = skillPolicy(loaded.parsed.name);
|
|
337
|
+
const policyBytes = Buffer.from(`${JSON.stringify(policy, null, 2)}\n`);
|
|
338
|
+
let image = null;
|
|
339
|
+
let hits = 0;
|
|
340
|
+
let receiptId = null;
|
|
341
|
+
if (files.length > 0) {
|
|
342
|
+
const core = await loadCore();
|
|
343
|
+
const objects = [];
|
|
344
|
+
for (const file of files) {
|
|
345
|
+
const bytes = await readFile(file.full);
|
|
346
|
+
objects.push({
|
|
347
|
+
kind: 'chunk',
|
|
348
|
+
bytes,
|
|
349
|
+
meta: { namespace: loaded.parsed.name, source: file.source },
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
image = core.createKnowledgeImageV5({
|
|
353
|
+
actor: 'knolo-skill',
|
|
354
|
+
policy,
|
|
355
|
+
objects,
|
|
356
|
+
});
|
|
357
|
+
const query = queryFlag ?? (await defaultQuery(files));
|
|
358
|
+
assertQuery(query);
|
|
359
|
+
const result = core.queryKnowledgeImageV5(
|
|
360
|
+
image,
|
|
361
|
+
`FROM chunk SEARCH ${JSON.stringify(query)} LIMIT 5`
|
|
362
|
+
);
|
|
363
|
+
hits = result.hits.length;
|
|
364
|
+
if (hits < 1) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
'SKILL_CHECK: the knowledge search returned no hits. Pass --query with words from the source.'
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
receiptId = `sr_${createHash('sha256')
|
|
370
|
+
.update(`${image.stateRoot}\n${result.resultRoot}`)
|
|
371
|
+
.digest('hex')
|
|
372
|
+
.slice(0, 24)}`;
|
|
373
|
+
}
|
|
374
|
+
const manifest = {
|
|
375
|
+
format: SKILL_PACK_FORMAT,
|
|
376
|
+
name: loaded.parsed.name,
|
|
377
|
+
description: loaded.parsed.description,
|
|
378
|
+
skillSha256: sha256(loaded.raw),
|
|
379
|
+
policySha256: sha256(policyBytes),
|
|
380
|
+
image: image
|
|
381
|
+
? { sha256: sha256(image.bytes), stateRoot: image.stateRoot }
|
|
382
|
+
: null,
|
|
383
|
+
};
|
|
384
|
+
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
385
|
+
assertNoHost(manifestBytes.toString('utf8'));
|
|
386
|
+
assertNoHost(policyBytes.toString('utf8'));
|
|
387
|
+
return {
|
|
388
|
+
manifest,
|
|
389
|
+
manifestBytes,
|
|
390
|
+
skillBytes: loaded.raw,
|
|
391
|
+
policyBytes,
|
|
392
|
+
imageBytes: image ? Buffer.from(image.bytes) : null,
|
|
393
|
+
image,
|
|
394
|
+
hits,
|
|
395
|
+
receiptId,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function skillPolicy(name) {
|
|
400
|
+
const principal = `skill:${name}`;
|
|
401
|
+
return {
|
|
402
|
+
version: 1,
|
|
403
|
+
default: 'deny',
|
|
404
|
+
rules: [
|
|
405
|
+
{ effect: 'allow', action: 'query', principal, kind: 'chunk' },
|
|
406
|
+
{ effect: 'allow', action: 'read', principal, kind: 'chunk' },
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function listKnowledge(dir) {
|
|
412
|
+
const root = path.join(dir, 'knowledge');
|
|
413
|
+
const info = await lstat(root);
|
|
414
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
415
|
+
throw new Error('SKILL_CHECK: knowledge/ must be a real directory.');
|
|
416
|
+
}
|
|
417
|
+
const files = [];
|
|
418
|
+
async function walk(current) {
|
|
419
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
420
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
421
|
+
for (const entry of entries) {
|
|
422
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
423
|
+
const full = path.join(current, entry.name);
|
|
424
|
+
const stat = await lstat(full);
|
|
425
|
+
if (stat.isSymbolicLink()) {
|
|
426
|
+
throw new Error(`SKILL_CHECK: refusing symlink ${entry.name}.`);
|
|
427
|
+
}
|
|
428
|
+
if (stat.isDirectory()) {
|
|
429
|
+
await walk(full);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (!stat.isFile()) continue;
|
|
433
|
+
if (!EXTENSIONS.has(path.extname(entry.name).toLowerCase())) continue;
|
|
434
|
+
if (stat.size > MAX_KNOWLEDGE_BYTES) {
|
|
435
|
+
throw new Error(`SKILL_CHECK: ${entry.name} is larger than 1 MiB.`);
|
|
436
|
+
}
|
|
437
|
+
const source = path.relative(root, full).split(path.sep).join('/');
|
|
438
|
+
files.push({ full, source });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
await walk(root);
|
|
442
|
+
if (files.length > MAX_KNOWLEDGE_FILES) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`SKILL_CHECK: a skill can contain ${MAX_KNOWLEDGE_FILES} knowledge files.`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
files.sort((left, right) => compareText(left.source, right.source));
|
|
448
|
+
return files;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function defaultQuery(files) {
|
|
452
|
+
const text = (await readFile(files[0].full)).toString('utf8');
|
|
453
|
+
const words = text
|
|
454
|
+
.toLowerCase()
|
|
455
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
456
|
+
.filter((word) => word.length >= 3)
|
|
457
|
+
.slice(0, 3);
|
|
458
|
+
if (words.length === 0) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
'SKILL_CHECK: pass --query. The first knowledge file has no words.'
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return words.join(' ');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function assertQuery(query) {
|
|
467
|
+
if (
|
|
468
|
+
typeof query !== 'string' ||
|
|
469
|
+
!/^[\p{L}\p{N} ]+$/u.test(query) ||
|
|
470
|
+
!query.trim()
|
|
471
|
+
) {
|
|
472
|
+
throw new Error('SKILL_CHECK: --query must be words separated by spaces.');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function writePack(out, built) {
|
|
477
|
+
await writePackFiles(out, {
|
|
478
|
+
manifestBytes: built.manifestBytes,
|
|
479
|
+
skillBytes: built.skillBytes,
|
|
480
|
+
policyBytes: built.policyBytes,
|
|
481
|
+
imageBytes: built.imageBytes,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function writePackFiles(out, pack) {
|
|
486
|
+
await mkdir(out, { recursive: true });
|
|
487
|
+
await writeFile(path.join(out, 'manifest.json'), pack.manifestBytes, {
|
|
488
|
+
mode: 0o644,
|
|
489
|
+
});
|
|
490
|
+
await writeFile(path.join(out, 'SKILL.md'), pack.skillBytes, { mode: 0o644 });
|
|
491
|
+
await writeFile(path.join(out, 'policy.json'), pack.policyBytes, {
|
|
492
|
+
mode: 0o644,
|
|
493
|
+
});
|
|
494
|
+
if (pack.imageBytes) {
|
|
495
|
+
await writeFile(path.join(out, 'knowledge.v5'), pack.imageBytes, {
|
|
496
|
+
mode: 0o644,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function readVerifiedPack(packDir) {
|
|
502
|
+
let manifestBytes;
|
|
503
|
+
let skillBytes;
|
|
504
|
+
let policyBytes;
|
|
505
|
+
try {
|
|
506
|
+
manifestBytes = await readFile(path.join(packDir, 'manifest.json'));
|
|
507
|
+
skillBytes = await readFile(path.join(packDir, 'SKILL.md'));
|
|
508
|
+
policyBytes = await readFile(path.join(packDir, 'policy.json'));
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (error && error.code === 'ENOENT') {
|
|
511
|
+
const missing = new Error(`SKILL_HUB: no skill pack at ${packDir}.`);
|
|
512
|
+
missing.code = 'ENOENT';
|
|
513
|
+
throw missing;
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
let manifest;
|
|
518
|
+
try {
|
|
519
|
+
manifest = JSON.parse(manifestBytes.toString('utf8'));
|
|
520
|
+
} catch {
|
|
521
|
+
throw new Error('SKILL_PIN: manifest.json is not valid JSON.');
|
|
522
|
+
}
|
|
523
|
+
if (!manifest || manifest.format !== SKILL_PACK_FORMAT) {
|
|
524
|
+
throw new Error('SKILL_PIN: manifest format is not a Knolo skill pack.');
|
|
525
|
+
}
|
|
526
|
+
const nameError = skillNameError(manifest.name);
|
|
527
|
+
if (nameError) throw new Error(nameError);
|
|
528
|
+
if (sha256(skillBytes) !== manifest.skillSha256) {
|
|
529
|
+
throw new Error('SKILL_PIN: SKILL.md does not match its SHA-256 pin.');
|
|
530
|
+
}
|
|
531
|
+
if (sha256(policyBytes) !== manifest.policySha256) {
|
|
532
|
+
throw new Error('SKILL_PIN: policy.json does not match its SHA-256 pin.');
|
|
533
|
+
}
|
|
534
|
+
const parsed = parseSkill(skillBytes.toString('utf8'));
|
|
535
|
+
if (
|
|
536
|
+
parsed.name !== manifest.name ||
|
|
537
|
+
parsed.description !== manifest.description
|
|
538
|
+
) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
'SKILL_PIN: SKILL.md does not match the manifest name and description.'
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const policy = parsePolicy(policyBytes);
|
|
544
|
+
let imageBytes = null;
|
|
545
|
+
if (manifest.image) {
|
|
546
|
+
try {
|
|
547
|
+
imageBytes = await readFile(path.join(packDir, 'knowledge.v5'));
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error && error.code === 'ENOENT') {
|
|
550
|
+
throw new Error('SKILL_PIN: knowledge.v5 is missing.');
|
|
551
|
+
}
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
if (sha256(imageBytes) !== manifest.image.sha256) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
'SKILL_PIN: knowledge.v5 does not match its SHA-256 pin.'
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const core = await loadCore();
|
|
560
|
+
let opened;
|
|
561
|
+
try {
|
|
562
|
+
opened = core.mountKnowledgeImageV5(imageBytes);
|
|
563
|
+
} catch {
|
|
564
|
+
throw new Error('SKILL_PIN: knowledge.v5 is not a V5 Knowledge Image.');
|
|
565
|
+
}
|
|
566
|
+
if (opened.stateRoot !== manifest.image.stateRoot) {
|
|
567
|
+
throw new Error('SKILL_PIN: knowledge.v5 does not match its state root.');
|
|
568
|
+
}
|
|
569
|
+
let policyRoot;
|
|
570
|
+
try {
|
|
571
|
+
policyRoot = core.knowledgePolicyRootV5(policy);
|
|
572
|
+
} catch {
|
|
573
|
+
throw new Error('SKILL_PIN: policy.json is not a V5 policy.');
|
|
574
|
+
}
|
|
575
|
+
if (opened.commit.policyRoot !== policyRoot) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
'SKILL_PIN: policy.json does not match the image policy root.'
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
try {
|
|
582
|
+
await lstat(path.join(packDir, 'knowledge.v5'));
|
|
583
|
+
throw new Error(
|
|
584
|
+
'SKILL_PIN: knowledge.v5 is present but the manifest has no image pin.'
|
|
585
|
+
);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (!(error && error.code === 'ENOENT')) throw error;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
assertNoHost(manifestBytes.toString('utf8'));
|
|
591
|
+
return { manifest, manifestBytes, skillBytes, policyBytes, imageBytes };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function packSummary(built, out) {
|
|
595
|
+
return {
|
|
596
|
+
ok: true,
|
|
597
|
+
name: built.manifest.name,
|
|
598
|
+
out,
|
|
599
|
+
skillSha256: built.manifest.skillSha256,
|
|
600
|
+
image: built.manifest.image,
|
|
601
|
+
receiptId: built.receiptId,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function sameImage(left, right) {
|
|
606
|
+
if (left === null || right === null) return left === right;
|
|
607
|
+
return left.sha256 === right.sha256 && left.stateRoot === right.stateRoot;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function assertNoHost(text) {
|
|
611
|
+
if (ABSOLUTE_PATH.test(text) || text.includes('managed-skills')) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
'SKILL_PACK: the pack names a home directory or a host layout.'
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function compareText(left, right) {
|
|
619
|
+
if (left < right) return -1;
|
|
620
|
+
if (left > right) return 1;
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function rejectHostPath(target) {
|
|
625
|
+
const home = process.env.HOME;
|
|
626
|
+
if (home && (target === home || target.startsWith(`${home}${path.sep}`))) {
|
|
627
|
+
// A user can install into a directory under their home. The pack itself
|
|
628
|
+
// must not record that path. This check only rejects the home directory
|
|
629
|
+
// as the catalog or install root.
|
|
630
|
+
if (target === home) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
'SKILL_PACK: pass a directory inside the destination, not the home directory itself.'
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function parsePolicy(policyBytes) {
|
|
639
|
+
try {
|
|
640
|
+
const policy = JSON.parse(policyBytes.toString('utf8'));
|
|
641
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
|
|
642
|
+
throw new Error('not an object');
|
|
643
|
+
}
|
|
644
|
+
return policy;
|
|
645
|
+
} catch {
|
|
646
|
+
throw new Error('SKILL_PIN: policy.json is not valid JSON.');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function sha256(bytes) {
|
|
651
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function loadCore() {
|
|
655
|
+
const candidates = [
|
|
656
|
+
path.resolve(__dirname, '../../core/dist/index.js'),
|
|
657
|
+
'@knolo/core',
|
|
658
|
+
];
|
|
659
|
+
for (const candidate of candidates) {
|
|
660
|
+
try {
|
|
661
|
+
const href = candidate.startsWith('@')
|
|
662
|
+
? candidate
|
|
663
|
+
: pathToFileURL(candidate).href;
|
|
664
|
+
const loaded = await import(href);
|
|
665
|
+
if (
|
|
666
|
+
loaded.createKnowledgeImageV5 &&
|
|
667
|
+
loaded.queryKnowledgeImageV5 &&
|
|
668
|
+
loaded.mountKnowledgeImageV5 &&
|
|
669
|
+
loaded.knowledgePolicyRootV5
|
|
670
|
+
) {
|
|
671
|
+
return loaded;
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
// Try the next candidate.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
throw new Error('Could not load @knolo/core. Build packages/core first.');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function parseArgs(argv) {
|
|
681
|
+
const flags = {};
|
|
682
|
+
const positionals = [];
|
|
683
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
684
|
+
const arg = argv[index];
|
|
685
|
+
if (arg === '--json') flags.json = true;
|
|
686
|
+
else if (arg === '--help' || arg === '-h') flags.help = true;
|
|
687
|
+
else if (arg.startsWith('--')) {
|
|
688
|
+
const key = arg.slice(2);
|
|
689
|
+
const value = argv[index + 1];
|
|
690
|
+
if (value === undefined || value.startsWith('--')) {
|
|
691
|
+
throw new Error(`Missing value for --${key}`);
|
|
692
|
+
}
|
|
693
|
+
flags[key] = value;
|
|
694
|
+
index += 1;
|
|
695
|
+
} else positionals.push(arg);
|
|
696
|
+
}
|
|
697
|
+
return { positionals, flags };
|
|
698
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knolo/cli",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"knolo": "bin/knolo.mjs"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "node -e \"console.log('cli is executable via bin/knolo.mjs')\"",
|
|
31
|
-
"test": "npm run build --prefix ../core && node --test test/cli.test.mjs test/kar.test.mjs test/kar-graph.test.mjs test/kar-produce.test.mjs test/hub.integration.test.mjs test/compose.test.mjs test/cortex.test.mjs",
|
|
31
|
+
"test": "npm run build --prefix ../core && node --test test/cli.test.mjs test/kar.test.mjs test/kar-graph.test.mjs test/kar-produce.test.mjs test/hub.integration.test.mjs test/compose.test.mjs test/cortex.test.mjs test/flight.test.mjs test/skill.test.mjs",
|
|
32
32
|
"test:hub": "node --test test/hub.integration.test.mjs"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|