@knolo/cli 6.1.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 +56 -4
- package/bin/cortex.mjs +125 -0
- package/bin/flight.mjs +367 -0
- package/bin/knolo.mjs +36 -0
- package/bin/skill.mjs +698 -0
- package/package.json +3 -3
- 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/README.md
CHANGED
|
@@ -12,13 +12,30 @@ the format is still draft and compressed object/event images require a VQF-aware
|
|
|
12
12
|
reader.
|
|
13
13
|
|
|
14
14
|
`knolo compose`, `knolo attest`, and `knolo verify-attestation` cite local
|
|
15
|
-
Knowledge Images and write an attestation sidecar. `@knolo/cli` `6.
|
|
16
|
-
depends on `@knolo/core` `^6.
|
|
15
|
+
Knowledge Images and write an attestation sidecar. `@knolo/cli` `6.2.0`
|
|
16
|
+
depends on `@knolo/core` `^6.2.0`. They do not fetch Hub. Verification
|
|
17
17
|
recomputes `compositionRoot` and checks bundled excerpt bytes. A keyring key
|
|
18
18
|
must fall inside `notBefore`, `notAfter`, and `revokedAt`. `used` means a
|
|
19
19
|
query or evaluation ran. It does not mean `SATISFIED`, and it does not mean a
|
|
20
20
|
claim is true or false. Hub `attested: true` is a listing flag.
|
|
21
21
|
|
|
22
|
+
`knolo cortex find`, `knolo cortex ask`, `knolo cortex explain`, and
|
|
23
|
+
`knolo cortex verify` run bounded evidence retrieval. The CLI does not call a
|
|
24
|
+
model and does not take an API key. A verified trace matches the image. It
|
|
25
|
+
does not mean the answer is true.
|
|
26
|
+
|
|
27
|
+
`knolo flight ingest` packs a `flight-events-1` document into a V5 Knowledge
|
|
28
|
+
Image and binds it to the SHA-256 of one log file. It does not decode the log.
|
|
29
|
+
`log.format` is `ulog`, `dataflash`, or `rosbag2`. Optional producers live in
|
|
30
|
+
`scripts/flight/` and are not part of `npm test`. `SATISFIED` on a later KAR
|
|
31
|
+
check means the evidence requirements were met. It does not mean a flight was
|
|
32
|
+
safe, legal, or compliant. See `docs/flight/README.md`.
|
|
33
|
+
|
|
34
|
+
`knolo skill` creates a skill pack for the host agent. The CLI does not call
|
|
35
|
+
a model. A receipt is an integrity check. It does not mean a claim is proven.
|
|
36
|
+
`SATISFIED` means the evidence requirements were met. See
|
|
37
|
+
[Skills](../../docs/skills/README.md).
|
|
38
|
+
|
|
22
39
|
The official CLI for building `.knolo` knowledge packs.
|
|
23
40
|
|
|
24
41
|
It indexes structured content and produces a deterministic, local-first knowledge bundle for use with `@knolo/core`.
|
|
@@ -115,6 +132,40 @@ Successful installs are cached by SHA-256 and recorded in
|
|
|
115
132
|
`knolo.lock.json`. A yanked version or an existing conflicting pin requires
|
|
116
133
|
`--force`; digest and artifact validation cannot be bypassed.
|
|
117
134
|
|
|
135
|
+
## Skill packs
|
|
136
|
+
|
|
137
|
+
`knolo skill` is separate from the Hub commands above. `knolo publish` still
|
|
138
|
+
uploads a `.knolo` knowledge pack to `https://hub.knolo.dev`. A skill pack
|
|
139
|
+
uses a directory you pass with `--hub`.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
knolo skill init refund-policy
|
|
143
|
+
knolo skill check refund-policy
|
|
144
|
+
knolo skill pack refund-policy --out dist/refund-policy.knolo-skill
|
|
145
|
+
knolo skill publish dist/refund-policy.knolo-skill --hub ./catalog
|
|
146
|
+
knolo skill install refund-policy --hub ./catalog --dir ./skills
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`init` writes `SKILL.md` and an empty `knowledge/` directory. `check` reads
|
|
150
|
+
that directory. When `knowledge/` has files, `check` builds a V5 image, runs
|
|
151
|
+
one search, and prints a receipt id. The receipt is an integrity check. It
|
|
152
|
+
does not mean a claim is proven.
|
|
153
|
+
|
|
154
|
+
`pack` writes `manifest.json`, `SKILL.md`, `policy.json`, and, when
|
|
155
|
+
`knowledge/` has files, `knowledge.v5`. The manifest stores SHA-256 pins and
|
|
156
|
+
the image state root. It does not store a home directory or a computer
|
|
157
|
+
layout. The same files produce the same pins.
|
|
158
|
+
|
|
159
|
+
`publish` copies the pack into `--hub/<name>`. The same pin can be published
|
|
160
|
+
again. A different pin is refused, and the catalog copy stays as it was.
|
|
161
|
+
`install` checks the pin before it writes. A mismatch writes nothing. The
|
|
162
|
+
catalog directory name must match the manifest. `SKILL.md` must match that
|
|
163
|
+
name and description, and `policy.json` must match the image policy root.
|
|
164
|
+
The files land only in `--dir/<name>`.
|
|
165
|
+
|
|
166
|
+
The directory catalog stores packs. It does not run a model, and it does not
|
|
167
|
+
answer questions. It is not `https://hub.knolo.dev`.
|
|
168
|
+
|
|
118
169
|
---
|
|
119
170
|
|
|
120
171
|
## 🚀 Commands
|
|
@@ -275,9 +326,10 @@ This ensures:
|
|
|
275
326
|
## 🗺 Roadmap
|
|
276
327
|
|
|
277
328
|
The active roadmap is maintained in [`../../docs/ROADMAP.md`](../../docs/ROADMAP.md).
|
|
278
|
-
The current release is `6.
|
|
329
|
+
The current release is `6.2.0`. It preserves the V4 CLI, keeps V5 verification,
|
|
279
330
|
migration, diagnostics, and read-only Studio management, and adds offline
|
|
280
|
-
composition and
|
|
331
|
+
composition, attestation, and `knolo cortex`. Cortex verification does not
|
|
332
|
+
mean an answer is true.
|
|
281
333
|
|
|
282
334
|
---
|
|
283
335
|
|
package/bin/cortex.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knolo cortex
|
|
3
|
+
*
|
|
4
|
+
* Bounded evidence retrieval. The CLI does not call a model and does not
|
|
5
|
+
* take an API key. Verification recomputes roots. It does not mean the
|
|
6
|
+
* answer is true.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
import { KAR_CLI_MAX_IMAGE_BYTES, KAR_CLI_MAX_TEXT_BYTES, readBounded, readBoundedJson } from './kar-limits.mjs';
|
|
12
|
+
|
|
13
|
+
const USAGE = `Usage:
|
|
14
|
+
knolo cortex find <image> --question "..." [--top-k 100] [--policy policy.json] --out candidates.json
|
|
15
|
+
knolo cortex ask <image> --question "..." [--graph file] [--plan plan.json] [--mode find_relevant] [--policy policy.json] [--created-at text] --out answer.cortex.json
|
|
16
|
+
knolo cortex explain <answer.cortex.json>
|
|
17
|
+
knolo cortex verify --image <image> --answer answer.cortex.json [--graph file] [--question "..."]
|
|
18
|
+
|
|
19
|
+
The CLI does not load a model. Hosts inject a model through createCortexSession.
|
|
20
|
+
Verification checks the trace against the image. It does not mean the answer is true.
|
|
21
|
+
SATISFIED on a KAR reference means evidence requirements were met. It does not mean a claim is proven.`;
|
|
22
|
+
|
|
23
|
+
export async function runCortexCli(args) {
|
|
24
|
+
const sub = args[0];
|
|
25
|
+
if (sub === 'find') return writeTrace(await runAsk(parseFlags(args.slice(1)), 'find_relevant'));
|
|
26
|
+
if (sub === 'ask') return writeTrace(await runAsk(parseFlags(args.slice(1)), undefined));
|
|
27
|
+
if (sub === 'explain') return explain(parseFlags(args.slice(1)));
|
|
28
|
+
if (sub === 'verify') return verify(parseFlags(args.slice(1)));
|
|
29
|
+
throw new Error(`Unknown cortex command.\n${USAGE}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runAsk(flags, forcedMode) {
|
|
33
|
+
const imagePath = flags._[0];
|
|
34
|
+
if (!imagePath) throw new Error(USAGE);
|
|
35
|
+
const question = required(flags, 'question');
|
|
36
|
+
const api = await loadCortex();
|
|
37
|
+
const policy = flags.policy ? readBoundedJson(flags.policy) : {};
|
|
38
|
+
if (flags['top-k'] !== undefined) {
|
|
39
|
+
const topK = Number(flags['top-k']);
|
|
40
|
+
if (!Number.isInteger(topK) || topK < 1) throw new Error('--top-k must be a positive integer.');
|
|
41
|
+
policy.maxCandidatesPerRound = topK;
|
|
42
|
+
}
|
|
43
|
+
const session = await api.createCortexSession({
|
|
44
|
+
image: new Uint8Array(readBounded(imagePath, KAR_CLI_MAX_IMAGE_BYTES)),
|
|
45
|
+
...(flags.graph ? { graph: readBoundedJson(flags.graph) } : {}),
|
|
46
|
+
...(flags.plan ? { plan: readBoundedJson(flags.plan) } : {}),
|
|
47
|
+
policy,
|
|
48
|
+
});
|
|
49
|
+
const mode = forcedMode ?? flags.mode ?? 'evidence_answer';
|
|
50
|
+
const trace = await api.askCortex(session, {
|
|
51
|
+
question,
|
|
52
|
+
mode,
|
|
53
|
+
...(flags['created-at'] ? { createdAt: flags['created-at'] } : {}),
|
|
54
|
+
});
|
|
55
|
+
return { flags, trace };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writeTrace(result) {
|
|
59
|
+
const json = `${JSON.stringify(result.trace, null, 2)}\n`;
|
|
60
|
+
if (result.flags.out) {
|
|
61
|
+
const out = path.resolve(process.cwd(), result.flags.out);
|
|
62
|
+
if (existsSync(out)) throw new Error(`Refusing to overwrite ${out}`);
|
|
63
|
+
writeFileSync(out, json);
|
|
64
|
+
console.log(`${result.trace.answer.status} ${result.trace.receipt.traceRoot}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(json);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function explain(flags) {
|
|
71
|
+
const file = flags._[0] ?? flags.answer;
|
|
72
|
+
if (!file) throw new Error(USAGE);
|
|
73
|
+
const api = await loadCortex();
|
|
74
|
+
const trace = readBoundedJson(file);
|
|
75
|
+
console.log(`${JSON.stringify(api.explainCortexAnswer(trace), null, 2)}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function verify(flags) {
|
|
79
|
+
const api = await loadCortex();
|
|
80
|
+
const answer = required(flags, 'answer');
|
|
81
|
+
const image = required(flags, 'image');
|
|
82
|
+
const trace = readBoundedJson(answer, KAR_CLI_MAX_TEXT_BYTES);
|
|
83
|
+
const verified = await api.verifyCortexAnswer({
|
|
84
|
+
image: new Uint8Array(readBounded(image, KAR_CLI_MAX_IMAGE_BYTES)),
|
|
85
|
+
question: flags.question ?? trace.question,
|
|
86
|
+
result: trace,
|
|
87
|
+
...(flags.graph ? { graph: readBoundedJson(flags.graph) } : {}),
|
|
88
|
+
});
|
|
89
|
+
if (!verified.ok) throw new Error(`${verified.code}: ${verified.message}`);
|
|
90
|
+
console.log(`ok ${verified.traceRoot}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadCortex() {
|
|
94
|
+
const candidate = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../core/dist/cortex/index.js');
|
|
95
|
+
const loaded = await import(pathToFileURL(candidate).href);
|
|
96
|
+
if (!loaded.createCortexSession || !loaded.askCortex || !loaded.verifyCortexAnswer) {
|
|
97
|
+
throw new Error('Could not load @knolo/core/cortex. Build packages/core first.');
|
|
98
|
+
}
|
|
99
|
+
return loaded;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function required(flags, name) {
|
|
103
|
+
if (!flags[name]) throw new Error(`Missing --${name}.\n${USAGE}`);
|
|
104
|
+
return flags[name];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseFlags(args) {
|
|
108
|
+
const flags = { _: [] };
|
|
109
|
+
for (let i = 0; i < args.length; i++) {
|
|
110
|
+
const arg = args[i];
|
|
111
|
+
if (arg === '--question') flags.question = args[++i];
|
|
112
|
+
else if (arg === '--graph') flags.graph = args[++i];
|
|
113
|
+
else if (arg === '--plan') flags.plan = args[++i];
|
|
114
|
+
else if (arg === '--mode') flags.mode = args[++i];
|
|
115
|
+
else if (arg === '--policy') flags.policy = args[++i];
|
|
116
|
+
else if (arg === '--out') flags.out = args[++i];
|
|
117
|
+
else if (arg === '--top-k') flags['top-k'] = args[++i];
|
|
118
|
+
else if (arg === '--created-at') flags['created-at'] = args[++i];
|
|
119
|
+
else if (arg === '--image') flags.image = args[++i];
|
|
120
|
+
else if (arg === '--answer') flags.answer = args[++i];
|
|
121
|
+
else if (arg.startsWith('--')) throw new Error(`Unknown flag ${arg}.\n${USAGE}`);
|
|
122
|
+
else flags._.push(arg);
|
|
123
|
+
}
|
|
124
|
+
return flags;
|
|
125
|
+
}
|
package/bin/flight.mjs
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knolo flight ingest
|
|
3
|
+
*
|
|
4
|
+
* Packs a flight-events-1 document into a V5 Knowledge Image.
|
|
5
|
+
* It does not decode the log. log.format is ulog, dataflash, or rosbag2.
|
|
6
|
+
* SATISFIED is not a statement that a flight was safe or compliant.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { createReadStream, existsSync, statSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
+
import { readBounded } from './kar-limits.mjs';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
export const FLIGHT_EVENTS_FORMAT = 'flight-events-1';
|
|
17
|
+
export const MAX_FLIGHT_EVENTS = 10_000;
|
|
18
|
+
export const MAX_FLIGHT_EVENT_BYTES = 2 * 1024 * 1024;
|
|
19
|
+
export const FLIGHT_ACTOR = 'knolo-flight';
|
|
20
|
+
|
|
21
|
+
const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
22
|
+
const PHRASE = /^[a-z0-9][a-z0-9 -]{0,160}$/;
|
|
23
|
+
const DIGEST = /^sha256-[0-9a-f]{64}$/;
|
|
24
|
+
const TOP_KEYS = ['format', 'parser', 'log', 'events'];
|
|
25
|
+
const EVENT_KEYS = ['id', 'kind', 'timestampUs', 'byteStart', 'byteEnd', 'fields', 'text'];
|
|
26
|
+
const KIND_FIELDS = {
|
|
27
|
+
arming: ['state'],
|
|
28
|
+
'nav-mode': ['mode'],
|
|
29
|
+
'failsafe-assert': ['name'],
|
|
30
|
+
'failsafe-clear': ['name'],
|
|
31
|
+
'geofence-breach': ['name'],
|
|
32
|
+
'geofence-clear': [],
|
|
33
|
+
'battery-threshold': ['level'],
|
|
34
|
+
'link-loss': [],
|
|
35
|
+
'link-restored': [],
|
|
36
|
+
'scan-summary': ['phrase'],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const USAGE = `Usage: knolo flight ingest --events <events.json> --log <file> --out <image.knolo> [--json]
|
|
40
|
+
|
|
41
|
+
Packs a flight-events-1 document into a V5 Knowledge Image.
|
|
42
|
+
The command hashes --log and refuses the image when the hash or byte length differs.
|
|
43
|
+
It does not decode a flight log. Parsing stays in an external producer.
|
|
44
|
+
A later KAR status of SATISFIED means evidence requirements were met.
|
|
45
|
+
It does not mean a flight was safe, legal, or compliant.`;
|
|
46
|
+
|
|
47
|
+
export async function runFlightCli(argv) {
|
|
48
|
+
const { positionals, flags } = parseArgs(argv);
|
|
49
|
+
const sub = positionals[0] ?? '';
|
|
50
|
+
if (!sub || sub === 'help' || flags.help) {
|
|
51
|
+
console.log(USAGE);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (sub !== 'ingest') throw new Error(`Unknown flight command "${sub}".\n${USAGE}`);
|
|
55
|
+
const eventsPath = required(flags, 'events');
|
|
56
|
+
const logPath = required(flags, 'log');
|
|
57
|
+
const outPath = required(flags, 'out');
|
|
58
|
+
const resolvedOut = path.resolve(process.cwd(), outPath);
|
|
59
|
+
if (existsSync(resolvedOut)) throw new Error(`Refusing to overwrite ${outPath}. Choose a new --out path.`);
|
|
60
|
+
const document = readEvents(eventsPath);
|
|
61
|
+
const log = await inspectLog(logPath);
|
|
62
|
+
if (log.byteLength !== document.log.byteLength || log.sha256 !== document.log.sha256) {
|
|
63
|
+
throw new Error('FLIGHT_LOG_MISMATCH: log bytes do not match events.log.sha256 or events.log.byteLength.');
|
|
64
|
+
}
|
|
65
|
+
const image = await flightImageFromDocument(document);
|
|
66
|
+
writeFileSync(resolvedOut, image.bytes);
|
|
67
|
+
if (flags.json) {
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
ok: true,
|
|
70
|
+
stateRoot: image.stateRoot,
|
|
71
|
+
events: document.events.length,
|
|
72
|
+
wrote: outPath,
|
|
73
|
+
}, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(`FLIGHT IMAGE\nstate root ${image.stateRoot}\nevents ${document.events.length}\nwrote ${outPath}\n\nEvidence requirements are not a safety or compliance determination.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readEvents(file) {
|
|
80
|
+
const resolved = path.resolve(process.cwd(), file);
|
|
81
|
+
const stat = statSync(resolved);
|
|
82
|
+
if (!stat.isFile()) throw new Error(`FLIGHT_EVENTS_INVALID: ${file} is not a file.`);
|
|
83
|
+
if (stat.size > MAX_FLIGHT_EVENT_BYTES) {
|
|
84
|
+
throw new Error(`FLIGHT_EVENTS_TOO_LARGE: ${file} is ${stat.size} bytes; limit is ${MAX_FLIGHT_EVENT_BYTES}.`);
|
|
85
|
+
}
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(readBounded(file, MAX_FLIGHT_EVENT_BYTES).toString('utf8'));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
throw new Error(`FLIGHT_EVENTS_INVALID: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
const document = parseFlightEvents(parsed);
|
|
94
|
+
if (!document.ok) throw new Error(`${document.code}: ${document.message}`);
|
|
95
|
+
return document.document;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseFlightEvents(value) {
|
|
99
|
+
if (!isPlain(value) || !sameKeys(value, TOP_KEYS)) {
|
|
100
|
+
return fail('FLIGHT_EVENTS_INVALID', 'Document must be flight-events-1 with parser, log, and events.');
|
|
101
|
+
}
|
|
102
|
+
if (value.format !== FLIGHT_EVENTS_FORMAT) {
|
|
103
|
+
return fail('FLIGHT_EVENTS_INVALID', 'format must be flight-events-1.');
|
|
104
|
+
}
|
|
105
|
+
const parser = parseParser(value.parser);
|
|
106
|
+
if (!parser.ok) return parser;
|
|
107
|
+
const log = parseLog(value.log);
|
|
108
|
+
if (!log.ok) return log;
|
|
109
|
+
if (!Array.isArray(value.events)) return fail('FLIGHT_EVENTS_INVALID', 'events must be an array.');
|
|
110
|
+
if (value.events.length > MAX_FLIGHT_EVENTS) {
|
|
111
|
+
return fail('FLIGHT_TOO_MANY_EVENTS', `event count ${value.events.length} exceeds ${MAX_FLIGHT_EVENTS}.`);
|
|
112
|
+
}
|
|
113
|
+
const events = [];
|
|
114
|
+
const ids = new Set();
|
|
115
|
+
for (const [index, raw] of value.events.entries()) {
|
|
116
|
+
const parsed = parseEvent(raw, index, log.log.byteLength);
|
|
117
|
+
if (!parsed.ok) return parsed;
|
|
118
|
+
if (ids.has(parsed.event.id)) return fail('FLIGHT_EVENTS_INVALID', `Duplicate event id ${parsed.event.id}.`);
|
|
119
|
+
ids.add(parsed.event.id);
|
|
120
|
+
events.push(parsed.event);
|
|
121
|
+
}
|
|
122
|
+
for (let index = 1; index < events.length; index += 1) {
|
|
123
|
+
if (compareEvents(events[index - 1], events[index]) > 0) {
|
|
124
|
+
return fail('FLIGHT_EVENTS_INVALID', 'events must be sorted by timestampUs, kind, byteStart, and id.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, document: { format: FLIGHT_EVENTS_FORMAT, parser: parser.parser, log: log.log, events } };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function renderEventText(event) {
|
|
131
|
+
const time = event.timestampUs;
|
|
132
|
+
const fields = event.fields;
|
|
133
|
+
switch (event.kind) {
|
|
134
|
+
case 'arming':
|
|
135
|
+
return `ARMING_STATE ${fields.state} at ${time} us.`;
|
|
136
|
+
case 'nav-mode':
|
|
137
|
+
return `NAV_STATE ${fields.mode} at ${time} us.`;
|
|
138
|
+
case 'failsafe-assert':
|
|
139
|
+
return `FAILSAFE ${fields.name} asserted at ${time} us.`;
|
|
140
|
+
case 'failsafe-clear':
|
|
141
|
+
return `FAILSAFE ${fields.name} cleared at ${time} us.`;
|
|
142
|
+
case 'geofence-breach':
|
|
143
|
+
return `GEOFENCE_BREACH ${fields.name} at ${time} us.`;
|
|
144
|
+
case 'geofence-clear':
|
|
145
|
+
return `GEOFENCE clear at ${time} us.`;
|
|
146
|
+
case 'battery-threshold':
|
|
147
|
+
return `battery threshold crossed at ${time} us. level ${fields.level}.`;
|
|
148
|
+
case 'link-loss':
|
|
149
|
+
return `link loss recorded at ${time} us.`;
|
|
150
|
+
case 'link-restored':
|
|
151
|
+
return `LINK_RESTORED at ${time} us.`;
|
|
152
|
+
case 'scan-summary':
|
|
153
|
+
return `${fields.phrase} at ${time} us.`;
|
|
154
|
+
default:
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function flightImageFromDocument(document) {
|
|
160
|
+
const api = await loadImageApi();
|
|
161
|
+
const encoder = new TextEncoder();
|
|
162
|
+
const objects = [
|
|
163
|
+
{
|
|
164
|
+
kind: 'metadata',
|
|
165
|
+
bytes: encoder.encode(metadataText(document)),
|
|
166
|
+
meta: {
|
|
167
|
+
source: 'flight-log.txt',
|
|
168
|
+
role: 'flight-log',
|
|
169
|
+
logSha256: document.log.sha256,
|
|
170
|
+
byteLength: String(document.log.byteLength),
|
|
171
|
+
parserId: document.parser.id,
|
|
172
|
+
parserVersion: document.parser.version,
|
|
173
|
+
eventCount: String(document.events.length),
|
|
174
|
+
logFormat: document.log.format,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
...document.events.map((event) => ({
|
|
178
|
+
kind: 'chunk',
|
|
179
|
+
bytes: encoder.encode(event.text),
|
|
180
|
+
meta: {
|
|
181
|
+
source: `${event.id}.txt`,
|
|
182
|
+
eventKind: event.kind,
|
|
183
|
+
timestampUs: String(event.timestampUs),
|
|
184
|
+
byteStart: String(event.byteStart),
|
|
185
|
+
byteEnd: String(event.byteEnd),
|
|
186
|
+
logSha256: document.log.sha256,
|
|
187
|
+
},
|
|
188
|
+
})),
|
|
189
|
+
];
|
|
190
|
+
return api.createKnowledgeImageV5({ actor: FLIGHT_ACTOR, sequence: 1, objects });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function hashFile(file) {
|
|
194
|
+
const resolved = path.resolve(process.cwd(), file);
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const hash = createHash('sha256');
|
|
197
|
+
const stream = createReadStream(resolved);
|
|
198
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
199
|
+
stream.on('error', reject);
|
|
200
|
+
stream.on('end', () => resolve(`sha256-${hash.digest('hex')}`));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function metadataText(document) {
|
|
205
|
+
return [
|
|
206
|
+
'format flight-events-1',
|
|
207
|
+
`log ${document.log.sha256}`,
|
|
208
|
+
`bytes ${document.log.byteLength}`,
|
|
209
|
+
`parser ${document.parser.id} ${document.parser.version}`,
|
|
210
|
+
`events ${document.events.length}`,
|
|
211
|
+
'',
|
|
212
|
+
].join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseParser(value) {
|
|
216
|
+
if (!isPlain(value) || !sameKeys(value, ['id', 'version'])) {
|
|
217
|
+
return fail('FLIGHT_EVENTS_INVALID', 'parser must contain id and version.');
|
|
218
|
+
}
|
|
219
|
+
if (typeof value.id !== 'string' || !TOKEN.test(value.id)) {
|
|
220
|
+
return fail('FLIGHT_EVENTS_INVALID', 'parser.id must be a short token.');
|
|
221
|
+
}
|
|
222
|
+
if (typeof value.version !== 'string' || !TOKEN.test(value.version)) {
|
|
223
|
+
return fail('FLIGHT_EVENTS_INVALID', 'parser.version must be a short token.');
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, parser: { id: value.id, version: value.version } };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseLog(value) {
|
|
229
|
+
if (!isPlain(value) || !sameKeys(value, ['sha256', 'byteLength', 'format'])) {
|
|
230
|
+
return fail('FLIGHT_EVENTS_INVALID', 'log must contain sha256, byteLength, and format.');
|
|
231
|
+
}
|
|
232
|
+
if (typeof value.sha256 !== 'string' || !DIGEST.test(value.sha256)) {
|
|
233
|
+
return fail('FLIGHT_EVENTS_INVALID', 'log.sha256 must be sha256- and 64 lowercase hex digits.');
|
|
234
|
+
}
|
|
235
|
+
if (!nonNegativeInteger(value.byteLength)) {
|
|
236
|
+
return fail('FLIGHT_EVENTS_INVALID', 'log.byteLength must be a non-negative integer.');
|
|
237
|
+
}
|
|
238
|
+
if (value.format !== 'ulog' && value.format !== 'dataflash' && value.format !== 'rosbag2') {
|
|
239
|
+
return fail('FLIGHT_EVENTS_INVALID', 'log.format must be ulog, dataflash, or rosbag2.');
|
|
240
|
+
}
|
|
241
|
+
return { ok: true, log: { sha256: value.sha256, byteLength: value.byteLength, format: value.format } };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseEvent(value, index, byteLength) {
|
|
245
|
+
const pathName = `events[${index}]`;
|
|
246
|
+
if (!isPlain(value) || !sameKeys(value, EVENT_KEYS)) {
|
|
247
|
+
return fail('FLIGHT_EVENTS_INVALID', `${pathName} has missing or unknown fields.`);
|
|
248
|
+
}
|
|
249
|
+
if (typeof value.id !== 'string' || !TOKEN.test(value.id)) {
|
|
250
|
+
return fail('FLIGHT_EVENTS_INVALID', `${pathName}.id must be a short token.`);
|
|
251
|
+
}
|
|
252
|
+
const fields = KIND_FIELDS[value.kind];
|
|
253
|
+
if (!fields) return fail('FLIGHT_EVENTS_INVALID', `${pathName}.kind is not a phase 1 event kind.`);
|
|
254
|
+
if (!nonNegativeInteger(value.timestampUs) || !nonNegativeInteger(value.byteStart) || !nonNegativeInteger(value.byteEnd)) {
|
|
255
|
+
return fail('FLIGHT_EVENTS_INVALID', `${pathName} timestamps and byte offsets must be non-negative integers.`);
|
|
256
|
+
}
|
|
257
|
+
if (value.byteEnd < value.byteStart || value.byteEnd > byteLength) {
|
|
258
|
+
return fail('FLIGHT_EVENTS_INVALID', `${pathName} byte range is outside the log.`);
|
|
259
|
+
}
|
|
260
|
+
const parsedFields = parseFields(value.fields, fields, pathName);
|
|
261
|
+
if (!parsedFields.ok) return parsedFields;
|
|
262
|
+
const event = {
|
|
263
|
+
id: value.id,
|
|
264
|
+
kind: value.kind,
|
|
265
|
+
timestampUs: value.timestampUs,
|
|
266
|
+
byteStart: value.byteStart,
|
|
267
|
+
byteEnd: value.byteEnd,
|
|
268
|
+
fields: parsedFields.fields,
|
|
269
|
+
text: '',
|
|
270
|
+
};
|
|
271
|
+
event.text = renderEventText(event);
|
|
272
|
+
if (value.text !== event.text) {
|
|
273
|
+
return fail('FLIGHT_EVENTS_INVALID', `${pathName}.text must be the canonical sentence for its kind and fields.`);
|
|
274
|
+
}
|
|
275
|
+
return { ok: true, event };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseFields(value, names, pathName) {
|
|
279
|
+
if (!isPlain(value) || !sameKeys(value, names)) {
|
|
280
|
+
return fail('FLIGHT_EVENTS_INVALID', `${pathName}.fields must contain exactly ${names.join(', ') || 'no keys'}.`);
|
|
281
|
+
}
|
|
282
|
+
const fields = {};
|
|
283
|
+
for (const name of names) {
|
|
284
|
+
const text = value[name];
|
|
285
|
+
const pattern = name === 'phrase' ? PHRASE : TOKEN;
|
|
286
|
+
if (typeof text !== 'string' || !pattern.test(text)) {
|
|
287
|
+
return fail('FLIGHT_EVENTS_INVALID', `${pathName}.fields.${name} is not a canonical token.`);
|
|
288
|
+
}
|
|
289
|
+
fields[name] = text;
|
|
290
|
+
}
|
|
291
|
+
return { ok: true, fields };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function compareEvents(left, right) {
|
|
295
|
+
if (left.timestampUs !== right.timestampUs) return left.timestampUs - right.timestampUs;
|
|
296
|
+
if (left.kind !== right.kind) return left.kind < right.kind ? -1 : 1;
|
|
297
|
+
if (left.byteStart !== right.byteStart) return left.byteStart - right.byteStart;
|
|
298
|
+
if (left.id !== right.id) return left.id < right.id ? -1 : 1;
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function fail(code, message) {
|
|
303
|
+
return { ok: false, code, message };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function nonNegativeInteger(value) {
|
|
307
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isPlain(value) {
|
|
311
|
+
return !!value && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function sameKeys(value, keys) {
|
|
315
|
+
const actual = Object.keys(value);
|
|
316
|
+
if (actual.length !== keys.length) return false;
|
|
317
|
+
const allowed = new Set(keys);
|
|
318
|
+
return actual.every((key) => allowed.has(key));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function inspectLog(file) {
|
|
322
|
+
const resolved = path.resolve(process.cwd(), file);
|
|
323
|
+
const stat = statSync(resolved);
|
|
324
|
+
if (!stat.isFile()) throw new Error(`FLIGHT_LOG_MISMATCH: ${file} is not a file.`);
|
|
325
|
+
const sha256 = await hashFile(file);
|
|
326
|
+
return { byteLength: stat.size, sha256 };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function loadImageApi() {
|
|
330
|
+
const candidates = [
|
|
331
|
+
path.resolve(__dirname, '../../core/dist/index.js'),
|
|
332
|
+
'@knolo/core',
|
|
333
|
+
];
|
|
334
|
+
for (const candidate of candidates) {
|
|
335
|
+
try {
|
|
336
|
+
const href = candidate.startsWith('@') ? candidate : pathToFileURL(candidate).href;
|
|
337
|
+
const loaded = await import(href);
|
|
338
|
+
if (loaded.createKnowledgeImageV5) return loaded;
|
|
339
|
+
} catch {
|
|
340
|
+
// Try the next candidate.
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
throw new Error('Could not load @knolo/core. Build packages/core first.');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseArgs(argv) {
|
|
347
|
+
const flags = {};
|
|
348
|
+
const positionals = [];
|
|
349
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
350
|
+
const arg = argv[index];
|
|
351
|
+
if (arg === '--json') flags.json = true;
|
|
352
|
+
else if (arg === '--help' || arg === '-h') flags.help = true;
|
|
353
|
+
else if (arg.startsWith('--')) {
|
|
354
|
+
const key = arg.slice(2);
|
|
355
|
+
const value = argv[index + 1];
|
|
356
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for --${key}`);
|
|
357
|
+
flags[key] = value;
|
|
358
|
+
index += 1;
|
|
359
|
+
} else positionals.push(arg);
|
|
360
|
+
}
|
|
361
|
+
return { positionals, flags };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function required(flags, name) {
|
|
365
|
+
if (!flags[name]) throw new Error(`Missing --${name}`);
|
|
366
|
+
return flags[name];
|
|
367
|
+
}
|
package/bin/knolo.mjs
CHANGED
|
@@ -76,6 +76,9 @@ const SUBCOMMANDS = new Set([
|
|
|
76
76
|
'compose',
|
|
77
77
|
'attest',
|
|
78
78
|
'verify-attestation',
|
|
79
|
+
'cortex',
|
|
80
|
+
'flight',
|
|
81
|
+
'skill',
|
|
79
82
|
]);
|
|
80
83
|
const ICP_SUBCOMMANDS = new Set([
|
|
81
84
|
'init',
|
|
@@ -173,6 +176,9 @@ Commands:
|
|
|
173
176
|
kar KAR evidence-set retrieval over a Committed Evidence Graph
|
|
174
177
|
compose Compose Knowledge Images, packs, and attestations offline
|
|
175
178
|
attest Create or verify a composition attestation
|
|
179
|
+
cortex Bounded evidence retrieval with a verifiable trace
|
|
180
|
+
flight Pack a flight-events-1 document into a V5 Knowledge Image
|
|
181
|
+
skill Create and share a Knolo skill pack
|
|
176
182
|
icp Scaffold and operate an ICP-native knowledge canister
|
|
177
183
|
|
|
178
184
|
Global options:
|
|
@@ -211,6 +217,24 @@ function printCommandHelp(command) {
|
|
|
211
217
|
kar: 'Usage: knolo kar <evaluate|verify|explain|inspect|graph|package|produce|domain|dossier|coverage|benchmark|demo|conformance> [options]\n\nKnolo Adversarial Retrieval. Semantics are frozen at kar-1-research-1.\n\n knolo kar evaluate --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..."\n knolo kar verify --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --result result.json\n knolo kar explain --result result.json\n knolo kar inspect --image knowledge.knolo --graph knowledge.kar.json [--plan plan.json]\n knolo kar graph build --image knowledge.knolo --source knowledge.ceg.yaml --out knowledge.kar.json\n knolo kar package --image knowledge.knolo --graph knowledge.kar.json --out dist/my-knowledge-kar/\n knolo kar produce rules --image knowledge.knolo --domain domains/contracts-v4 --out proposals.json\n knolo kar domain validate domains/contracts-v4\n knolo kar dossier build --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --out dossier/\n knolo kar dossier verify dossier/\n knolo kar coverage build --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --out coverage/\n knolo kar coverage verify coverage/\n knolo kar benchmark annotate --packet packet.json --decisions decisions.json --out labels.json --role human --reviewer-id <id>\n knolo kar demo\n knolo kar conformance\n\n--json prints the machine-readable result. verify exits non-zero when the certificate does not match. Abstention is a result and exits 0. graph and package author a Committed Evidence Graph without changing retrieval. produce proposes CEG Source and does not write committed bytes. dossier packages one evaluation and fails closed when committed fields change. coverage reports met requirements, gaps, and plans that did not decide. A gap is not a false claim. benchmark labels gold without showing producer output. demo and conformance run from a knolo-core checkout.',
|
|
212
218
|
compose: 'Usage: knolo compose <create|verify|excerpt|query> [options]\n\nOffline composition of Knowledge Images, packs, KAR sidecars, Domain Packs, and bounded excerpts.\n\n knolo compose create --members members.json --out composition/ [--host knowledge.knolo] [--availability material|digest] [--policy policy.json]\n knolo compose verify composition/ [--require-material] [--member alias=path]\n knolo compose excerpt --image knowledge.knolo --object <id> --out excerpt.json\n knolo compose query --composition composition/ --eql "FROM chunk SEARCH \\"valve\\"" [--question "..."] [--json]\n\ncompositionRoot does not change a V5 state root or a KAR root. statement used means a query or evaluation ran. It does not mean SATISFIED, and it does not mean a claim is true or false. Hub attested: true is a listing flag and is not an attestation root. The CLI does not fetch Hub.',
|
|
213
219
|
attest: 'Usage: knolo attest <create|verify> [options]\n\n knolo attest create --composition composition/composition.json --subject composition --statement mount|used|cited --out attestation.json [--sign-key key.json --principal <id> --key-id <id> --issued-at <ms> --expires-at <ms>]\n knolo attest verify attestation.json [--composition composition.json] [--public-key key.json] [--now <ms>] [--content-only]\n\nstatement used means a query or KAR evaluation ran. It does not mean the claim is true or false. Hub attested: true is unrelated. Signing uses the supplied times. The CLI does not read the clock.',
|
|
220
|
+
cortex: 'Usage: knolo cortex <find|ask|explain|verify> [options]\n\nBounded evidence retrieval. The CLI does not call a model.\n\n knolo cortex find <image> --question "..." [--top-k 100] --out candidates.json\n knolo cortex ask <image> --question "..." [--graph file] [--plan plan.json] [--mode find_relevant] --out answer.cortex.json\n knolo cortex explain answer.cortex.json\n knolo cortex verify --image <image> --answer answer.cortex.json [--graph file]\n\nVerification recomputes the trace roots. It does not mean the answer is true. A KAR status of SATISFIED means evidence requirements were met. It does not mean a claim is proven.',
|
|
221
|
+
flight: 'Usage: knolo flight ingest --events <events.json> --log <file> --out <image.knolo> [--json]\n\nPacks a flight-events-1 document into a V5 Knowledge Image. The command hashes --log and writes nothing when the hash or byte length differs. It does not decode the log. log.format is ulog, dataflash, or rosbag2. A later KAR status of SATISFIED means evidence requirements were met. It does not mean a flight was safe, legal, or compliant.',
|
|
222
|
+
skill: `Usage: knolo skill <init|check|pack|publish|install>
|
|
223
|
+
|
|
224
|
+
knolo skill init <name> [--dir <parent>]
|
|
225
|
+
knolo skill check [<dir>] [--query <text>] [--json]
|
|
226
|
+
knolo skill pack [<dir>] [--out <dir>] [--json]
|
|
227
|
+
knolo skill publish <pack> --hub <dir> [--json]
|
|
228
|
+
knolo skill install <name> --hub <dir> --dir <path> [--json]
|
|
229
|
+
knolo skill install --pack <pack> --dir <path> [--json]
|
|
230
|
+
|
|
231
|
+
Creates a skill pack for the host agent. Knolo does not call a model.
|
|
232
|
+
A receipt is an integrity check. It does not mean a claim is proven.
|
|
233
|
+
SATISFIED means the evidence requirements were met.
|
|
234
|
+
publish copies the pack into a directory catalog. install checks the pin
|
|
235
|
+
and writes only into --dir. A pin mismatch writes nothing.
|
|
236
|
+
The catalog stores packs and does not answer questions.
|
|
237
|
+
It is not https://hub.knolo.dev.`,
|
|
214
238
|
'verify-attestation': 'Usage: knolo verify-attestation <attestation.json> [--composition composition.json] [--public-key key.json] [--keyring keyring.json] [--now <ms>] [--content-only]\n\nSame check as knolo attest verify. A bad signature exits 1 even when the content root matches. --content-only checks the content root and ignores the envelope. statement used is not a truth verdict. Hub attested: true is unrelated.',
|
|
215
239
|
icp: 'Usage: knolo icp <command> [options]',
|
|
216
240
|
'semantic:index':
|
|
@@ -1865,6 +1889,18 @@ async function main() {
|
|
|
1865
1889
|
const { runComposeCli } = await import('./compose.mjs');
|
|
1866
1890
|
return runComposeCli(command, commandArgs);
|
|
1867
1891
|
}
|
|
1892
|
+
if (command === 'cortex') {
|
|
1893
|
+
const { runCortexCli } = await import('./cortex.mjs');
|
|
1894
|
+
return runCortexCli(commandArgs);
|
|
1895
|
+
}
|
|
1896
|
+
if (command === 'flight') {
|
|
1897
|
+
const { runFlightCli } = await import('./flight.mjs');
|
|
1898
|
+
return runFlightCli(commandArgs);
|
|
1899
|
+
}
|
|
1900
|
+
if (command === 'skill') {
|
|
1901
|
+
const { runSkillCli } = await import('./skill.mjs');
|
|
1902
|
+
return runSkillCli(commandArgs);
|
|
1903
|
+
}
|
|
1868
1904
|
if (command === 'init') return await cmdInit();
|
|
1869
1905
|
if (command === 'add') return await cmdAdd(commandArgs);
|
|
1870
1906
|
if (command === 'search') return await runHubSearch(commandArgs);
|