@josfox/jos 4.0.8 → 5.0.0
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/LICENSE +208 -21
- package/PROMISES.md +52 -0
- package/README.md +294 -146
- package/SECURITY-MCP.md +117 -0
- package/SECURITY-SECRETS.md +108 -0
- package/STYLE.md +23 -0
- package/bin/jos +139 -67
- package/bin/jos.cmd +5 -0
- package/examples/01-hello.jos +44 -0
- package/examples/02-criteria.jos +54 -0
- package/examples/03-flow-gate.jos +83 -0
- package/examples/04-use-template/README.md +22 -0
- package/examples/04-use-template/lib/greeting.jos +56 -0
- package/examples/04-use-template/use.jos +64 -0
- package/examples/05-sealed/.jos/manifest.jos +53 -0
- package/examples/05-sealed/README.md +25 -0
- package/examples/06-requires.jos +42 -0
- package/examples/07-sandbox.jos +39 -0
- package/examples/08-ids-ledger-meter.md +26 -0
- package/examples/10-ollama.jos +47 -0
- package/examples/11-openai.jos +47 -0
- package/examples/12-gemini.jos +47 -0
- package/examples/13-anthropic.jos +47 -0
- package/examples/14-serve.jos +88 -0
- package/examples/README.md +105 -0
- package/examples/hello.jos +47 -0
- package/package.json +31 -9
- package/promises.json +894 -0
- package/src/args.js +47 -0
- package/src/commands/_frozen/add.README.md +1 -0
- package/src/commands/_frozen/get.README.md +1 -0
- package/src/commands/_frozen/llm.README.md +1 -0
- package/src/commands/_frozen/prompts.README.md +1 -0
- package/src/commands/_frozen/repo.README.md +1 -0
- package/src/commands/_frozen/serve-explorer.README.md +1 -0
- package/src/commands/ids.js +101 -0
- package/src/commands/init.js +113 -147
- package/src/commands/ledger.js +94 -0
- package/src/commands/meter.js +96 -0
- package/src/commands/migrate.js +214 -0
- package/src/commands/overview.js +118 -0
- package/src/commands/run.js +164 -214
- package/src/commands/runs.js +102 -0
- package/src/commands/secrets.js +695 -115
- package/src/commands/start.js +210 -0
- package/src/commands/validate.js +153 -165
- package/src/commands/verify.js +56 -0
- package/src/commands/version.js +23 -0
- package/src/intention.js +114 -0
- package/src/kernel.js +69 -0
- package/src/ledgers.js +57 -0
- package/src/mcp-guide.js +138 -0
- package/src/mcp.js +734 -0
- package/src/serve.js +38 -770
- package/src/ui.js +98 -0
- package/src/commands/add.js +0 -145
- package/src/commands/get.js +0 -245
- package/src/commands/repo.js +0 -139
- package/src/index.js +0 -0
package/src/args.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Command-line splitting shared by the commands. `--` ends the options: everything after it is a file,
|
|
3
|
+
// so `jos run -- -odd-name.jos` works. A lone `-` is a file name, not an option.
|
|
4
|
+
function split(args) {
|
|
5
|
+
const flags = [];
|
|
6
|
+
const files = [];
|
|
7
|
+
let positional = false;
|
|
8
|
+
for (const a of args) {
|
|
9
|
+
if (positional) files.push(a);
|
|
10
|
+
else if (a === '--') positional = true;
|
|
11
|
+
else if (a.length > 1 && a.startsWith('-')) flags.push(a);
|
|
12
|
+
else files.push(a);
|
|
13
|
+
}
|
|
14
|
+
return { flags, files, help: flags.includes('--help') || flags.includes('-h') };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Like split, with options that take a value. spec maps an option to false (a switch), true (one value, the last one
|
|
18
|
+
// given wins) or 'many' (repeatable, values in order). Returns { opts, files, help, error }: opts[name] is true, a
|
|
19
|
+
// string or an array; error is the first problem found (an unknown option, or an option without its value), or null.
|
|
20
|
+
function parse(args, spec) {
|
|
21
|
+
const opts = {};
|
|
22
|
+
const files = [];
|
|
23
|
+
let help = false;
|
|
24
|
+
let error = null;
|
|
25
|
+
let positional = false;
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i];
|
|
28
|
+
if (positional) { files.push(a); continue; }
|
|
29
|
+
if (a === '--') { positional = true; continue; }
|
|
30
|
+
if (!(a.length > 1 && a.startsWith('-'))) { files.push(a); continue; }
|
|
31
|
+
if (a === '--help' || a === '-h') { help = true; continue; }
|
|
32
|
+
if (!Object.prototype.hasOwnProperty.call(spec, a)) { if (!error) error = { option: a, kind: 'unknown' }; continue; }
|
|
33
|
+
if (spec[a] === false) { opts[a] = true; continue; }
|
|
34
|
+
if (i + 1 >= args.length) { if (!error) error = { option: a, kind: 'value' }; continue; }
|
|
35
|
+
const v = args[++i];
|
|
36
|
+
if (spec[a] === 'many') (opts[a] = opts[a] || []).push(v);
|
|
37
|
+
else opts[a] = v;
|
|
38
|
+
}
|
|
39
|
+
return { opts, files, help, error };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The message for parse()'s error: "unknown option --x" or "option needs a value: --flow".
|
|
43
|
+
function problem(error) {
|
|
44
|
+
return (error.kind === 'unknown' ? 'unknown option ' : 'option needs a value: ') + error.option;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { split, parse, problem };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Frozen in 5.0.0: `jos add` edited 0.0.7 `orchestration.definitions` and `orchestration.flows` and regenerated their `.sig.json` manifest (`jos run` verifies such a manifest again since jos-core 2.0.0-rc.8; `jos migrate` reseals the document as 2.0). jos-core runs 2.0 flows (`orchestration`, `use`, `needs`, gates); write them in the document (`jos start` writes one, `flow.jos`, and examples/03-flow-gate.jos is another). No date is set for `add`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Frozen in 5.0.0: downloads a .jos from a repo without verifying it and writes to a path built from meta.name (path traversal); to be rewritten on PAL/Store in F2/F3.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Frozen in 5.0.0: overwrites the .jos file with model output without validating it and sends the artifact with the encrypted vault file as the Bearer token; to be rewritten on PAL/Store in F2/F3.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Frozen in 5.0.0: `optimize` overwrites the .jos file with model output without validating it, and the adapters fabricate quality scores; to be rewritten on PAL/Store in F2/F3.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Frozen in 5.0.0: manages ~/.jos/repos.json for the frozen `get`, which fetches unverified artifacts over plain HTTP; to be rewritten on PAL/Store in F2/F3.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Frozen in 5.0.0: the file explorer served files by cwd without a sandbox and exposed ~/.jos/secrets when cwd was HOME (the live src/serve.js keeps only `--mcp`); to be rewritten in F3.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOS IDS - the identifiers jos-core gives each document (JOSID): the content id art_<sha256 of the canonical
|
|
3
|
+
* document> (SPEC-2.0 §8.2) and the entity id ent_ = uuid5("<jos.kind>:<id>") of the name. For a folder with a
|
|
4
|
+
* .jos/ directory, also the directory hash that seals it (SPEC-2.0 §9.3). Nothing runs.
|
|
5
|
+
* Exit codes: 0 every document valid, 2 one or more invalid, 5 no kernel, 64 usage.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const kernel = require('../kernel');
|
|
10
|
+
const ui = require('../ui');
|
|
11
|
+
const { RULE } = ui;
|
|
12
|
+
const { parse, problem } = require('../args');
|
|
13
|
+
|
|
14
|
+
const USAGE = 'usage: jos ids <file.jos | folder>... [--json]';
|
|
15
|
+
|
|
16
|
+
const HELP = (C) => `
|
|
17
|
+
${C.purple}${RULE}${C.reset}
|
|
18
|
+
${C.bold}JOS IDS${C.reset} // Content and entity identifiers of .jos documents
|
|
19
|
+
${C.purple}${RULE}${C.reset}
|
|
20
|
+
|
|
21
|
+
${C.bold}Usage:${C.reset}
|
|
22
|
+
jos ids <file.jos | folder>... [--json]
|
|
23
|
+
|
|
24
|
+
artifact art_ + SHA-256 of the canonical document (RFC 8785): changes when any byte
|
|
25
|
+
of meaning changes; the same in both reference kernels
|
|
26
|
+
entity ent_ = UUIDv5 of "<jos.kind>:<id>": stays when the content changes
|
|
27
|
+
directory for a folder D with D/.jos/manifest.jos: the hash over D/.jos/ that
|
|
28
|
+
integrity.hash must match for the folder to run (sealed)
|
|
29
|
+
|
|
30
|
+
${C.bold}Options:${C.reset}
|
|
31
|
+
--json Print one object per document (file, valid, artifact, kind, id, ent)
|
|
32
|
+
|
|
33
|
+
${C.bold}Exit codes:${C.reset}
|
|
34
|
+
0 all valid, 2 one or more invalid, 5 no kernel, 64 usage
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
function idsOf(core, given) {
|
|
38
|
+
const abs = path.resolve(given);
|
|
39
|
+
const file = core.documentPath(abs);
|
|
40
|
+
const out = { file: given, valid: false, artifact: null, kind: null, id: null, ent: null };
|
|
41
|
+
let v;
|
|
42
|
+
try { v = core.validate(core.loadFile(file)); } catch (e) {
|
|
43
|
+
if (!(e instanceof core.JosError)) throw e;
|
|
44
|
+
return Object.assign(out, { errors: e.errors });
|
|
45
|
+
}
|
|
46
|
+
out.valid = v.valid;
|
|
47
|
+
out.artifact = v.artifact;
|
|
48
|
+
if (!v.valid) return Object.assign(out, { errors: v.errors });
|
|
49
|
+
out.kind = v.document.jos.kind;
|
|
50
|
+
out.id = typeof v.document.id === 'string' ? v.document.id : null;
|
|
51
|
+
if (out.id !== null) out.ent = core.josid.named(out.kind, out.id);
|
|
52
|
+
if (file !== abs || path.basename(file) === 'manifest.jos') {
|
|
53
|
+
const jd = path.dirname(file);
|
|
54
|
+
const declared = v.document.integrity && typeof v.document.integrity.hash === 'string' ? v.document.integrity.hash : null;
|
|
55
|
+
out.directory = { hash: core.dirHash(jd).hash, sealed: declared !== null };
|
|
56
|
+
if (declared !== null) out.directory.matches = declared === out.directory.hash;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
exports.execute = async (args) => {
|
|
62
|
+
const err = ui.printer(process.stderr);
|
|
63
|
+
const { opts, files, help, error } = parse(args, { '--json': false });
|
|
64
|
+
if (help) {
|
|
65
|
+
const out = ui.printer(process.stdout);
|
|
66
|
+
out.text(HELP(out.C));
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (error || !files.length) {
|
|
70
|
+
err.fail('jos ids: ' + (error ? problem(error) : 'takes one or more files'));
|
|
71
|
+
err.hint(USAGE);
|
|
72
|
+
return 64;
|
|
73
|
+
}
|
|
74
|
+
let k;
|
|
75
|
+
let core;
|
|
76
|
+
try { k = kernel.locate(); core = kernel.need(k, ['validate', 'loadFile', 'documentPath', 'josid.named', 'dirHash']); } catch (e) { return kernel.fail(e); }
|
|
77
|
+
const rows = files.map((f) => idsOf(core, f));
|
|
78
|
+
const code = rows.every((r) => r.valid) ? 0 : 2;
|
|
79
|
+
if (opts['--json']) {
|
|
80
|
+
ui.json(process.stdout, rows);
|
|
81
|
+
return code;
|
|
82
|
+
}
|
|
83
|
+
const out = ui.printer(process.stdout);
|
|
84
|
+
out.header('JOS IDS', 'jos-core ' + k.version);
|
|
85
|
+
for (const r of rows) {
|
|
86
|
+
if (!r.valid) {
|
|
87
|
+
out.fail(r.file, '(invalid)');
|
|
88
|
+
for (const e of r.errors) out.hint(e);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
out.ok(r.file, '(' + r.kind + (r.id === null ? '' : ' ' + r.id) + ')');
|
|
92
|
+
out.field('artifact', r.artifact);
|
|
93
|
+
out.field('entity', r.ent === null ? 'none (the document has no id)' : r.ent);
|
|
94
|
+
if (r.directory) {
|
|
95
|
+
const seal = !r.directory.sealed ? 'not sealed (no integrity.hash)' : r.directory.matches ? 'sealed, matches' : 'sealed, does NOT match: jos run refuses it (code 3)';
|
|
96
|
+
out.field('directory', r.directory.hash + ' (' + seal + ')');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.line();
|
|
100
|
+
return code;
|
|
101
|
+
};
|
package/src/commands/init.js
CHANGED
|
@@ -1,162 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JOS INIT
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* JOS INIT - write a new .jos document that `jos run` accepts today.
|
|
3
|
+
* 5.0.0 writes a 2.0.0 atom (SPEC-2.0 §3), validated by jos-core before it touches the disk; 4.1.0 wrote a
|
|
4
|
+
* 0.0.7 document with flows plus a `.sig.json` manifest (verified by run; jos migrate reseals such a document).
|
|
5
|
+
* Exit codes: 0 written, 1 file exists or cannot be written, 5 no kernel, 64 usage.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
+
'use strict';
|
|
7
8
|
const fs = require('fs');
|
|
8
9
|
const path = require('path');
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
${C.purple}
|
|
10
|
+
const kernel = require('../kernel');
|
|
11
|
+
const ui = require('../ui');
|
|
12
|
+
const { RULE } = ui;
|
|
13
|
+
const { split } = require('../args');
|
|
14
|
+
|
|
15
|
+
const NAME = /^[a-z][a-z0-9._-]*$/; // same alphabet as the document id (SPEC-2.0 §2.7)
|
|
16
|
+
const EXT = /\.jos(\.json)?$/; // a name that already ends in .jos or .jos.json gets no second extension
|
|
17
|
+
|
|
18
|
+
// "hola" -> hola.jos, "hola.jos" -> hola.jos, "hola.jos.json" -> hola.jos.json, "dir/hola.jos" -> dir/hola.jos;
|
|
19
|
+
// the id is the file name without its extension and without the directory: "hola" in all four.
|
|
20
|
+
function target(given) {
|
|
21
|
+
const ext = EXT.test(given) ? '' : '.jos';
|
|
22
|
+
const shown = given + ext;
|
|
23
|
+
return { shown, file: path.resolve(shown), id: path.basename(given).replace(EXT, '') };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const HELP = (C) => `
|
|
27
|
+
${C.purple}${RULE}${C.reset}
|
|
28
|
+
${C.bold}JOS INIT${C.reset} // Write a new .jos 2.0 atom
|
|
29
|
+
${C.purple}${RULE}${C.reset}
|
|
27
30
|
|
|
28
31
|
${C.bold}Usage:${C.reset}
|
|
29
|
-
jos init <name> [
|
|
30
|
-
|
|
31
|
-
${C.bold}Options:${C.reset}
|
|
32
|
-
--pipeline Create a pipeline artifact (default: atom)
|
|
33
|
-
|
|
34
|
-
${C.bold}Creates:${C.reset}
|
|
35
|
-
<name>.jos The artifact file
|
|
36
|
-
<name>.jos.sig.json Detached integrity manifest
|
|
37
|
-
`);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
32
|
+
jos init <name> [--force]
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"$schema": "https://josfox.ai/schemas/jos-0.0.7.json",
|
|
48
|
-
"$comment": "Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)",
|
|
49
|
-
|
|
50
|
-
// JOSFOXAI Kernel
|
|
51
|
-
"jos": {
|
|
52
|
-
"open": `jos run ${type}`,
|
|
53
|
-
"supports": ["@josfox/jos", "josctl"],
|
|
54
|
-
"canonical": "Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)"
|
|
55
|
-
},
|
|
56
|
-
"orchestration_contract": {
|
|
57
|
-
"version": "0.0.7",
|
|
58
|
-
"mode": "sync"
|
|
59
|
-
},
|
|
60
|
-
"id_jos": `${type}-${name}-${Date.now()}`,
|
|
61
|
-
"adaptive_ai": {
|
|
62
|
-
"learning_mode": "frozen",
|
|
63
|
-
"llm_friendly_sentence": `This ${type} artifact performs the ${name} task.`
|
|
64
|
-
},
|
|
65
|
-
"security": {
|
|
66
|
-
"type": "open",
|
|
67
|
-
"permissions": [],
|
|
68
|
-
"health_check": null,
|
|
69
|
-
"integrity": null,
|
|
70
|
-
"integrity_ref": `./${filename}.sig.json`
|
|
71
|
-
},
|
|
72
|
-
"files": [],
|
|
73
|
-
"orchestration": {
|
|
74
|
-
"state": { "current_status": "idle" },
|
|
75
|
-
"definitions": {
|
|
76
|
-
"main_task": {
|
|
77
|
-
"type": "shell",
|
|
78
|
-
"description": "Main task - replace with your command",
|
|
79
|
-
"command": "echo 'Hello from " + name + "'"
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
"flows": {
|
|
83
|
-
"main": {
|
|
84
|
-
"description": "Main execution flow",
|
|
85
|
-
"steps": ["main_task"]
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
"x_run_params": {
|
|
90
|
-
"timeout": 300,
|
|
91
|
-
"retry_on_failure": false
|
|
92
|
-
},
|
|
34
|
+
${C.bold}Writes:${C.reset}
|
|
35
|
+
<name>.jos a 2.0.0 atom: intention, success criteria, guardrails and a shell step,
|
|
36
|
+
validated by jos-core before writing; run it with \`jos run <name>.jos\`
|
|
37
|
+
A name that already ends in .jos or .jos.json is used as is:
|
|
38
|
+
\`jos init hola.jos\` writes hola.jos, \`jos init dir/hola\` writes dir/hola.jos
|
|
39
|
+
(dir must exist).
|
|
93
40
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
41
|
+
${C.bold}Options:${C.reset}
|
|
42
|
+
--force overwrite the file if it exists
|
|
43
|
+
|
|
44
|
+
${C.bold}Name:${C.reset} the file name without .jos / .jos.json becomes the document id: lowercase letter
|
|
45
|
+
first, then letters, digits, '.', '_' or '-'
|
|
46
|
+
${C.dim}--pipeline was removed: init writes atoms only. For a flow, see the flow.jos that jos start writes.${C.reset}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
function template(name) {
|
|
50
|
+
return {
|
|
51
|
+
jos: { version: '2.0.0', kind: 'atom' },
|
|
52
|
+
id: name,
|
|
53
|
+
meta: { name, version: '0.1.0', description: 'Replace this description.' },
|
|
54
|
+
intention: {
|
|
55
|
+
objective: 'Describe what done means for ' + name + '.',
|
|
56
|
+
success_criteria: [{ exit: 0 }, { stdout_contains: 'Hello from ' + name }],
|
|
106
57
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
58
|
+
guardrails: {
|
|
59
|
+
avoid: ['rm -rf', 'curl', 'wget'],
|
|
60
|
+
limit: { seconds: 30, output_bytes: 65536, retries: 0 },
|
|
110
61
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
62
|
+
execution: {
|
|
63
|
+
engine: 'shell',
|
|
64
|
+
run: ['echo', 'Hello from ' + name],
|
|
65
|
+
on_error: 'fail',
|
|
66
|
+
env: ['PATH', 'HOME'],
|
|
67
|
+
cwd: '.',
|
|
114
68
|
},
|
|
115
|
-
"capabilities": []
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Write artifact
|
|
119
|
-
const content = JSON.stringify(template, null, 2);
|
|
120
|
-
fs.writeFileSync(filename, content);
|
|
121
|
-
console.log(`${C.green}✓ Created:${C.reset} ${filename}`);
|
|
122
|
-
|
|
123
|
-
// Generate integrity manifest
|
|
124
|
-
const sha256 = crypto.createHash('sha256').update(content).digest('hex');
|
|
125
|
-
const stats = fs.statSync(filename);
|
|
126
|
-
|
|
127
|
-
const manifest = {
|
|
128
|
-
"$schema": "https://josfox.ai/schemas/jos-manifest-0.0.7.json",
|
|
129
|
-
"manifest_version": "1.0.0",
|
|
130
|
-
"artifact_ref": filename,
|
|
131
|
-
"artifact_sha256": sha256,
|
|
132
|
-
"artifact_size_bytes": stats.size,
|
|
133
|
-
"timestamp": new Date().toISOString(),
|
|
134
|
-
"generated_by": "jos init command",
|
|
135
|
-
"canonical_version": "Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)"
|
|
136
69
|
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
exports.execute = async (args) => {
|
|
73
|
+
const err = ui.printer(process.stderr);
|
|
74
|
+
const { flags, files, help } = split(args);
|
|
75
|
+
if (help) {
|
|
76
|
+
const out = ui.printer(process.stdout);
|
|
77
|
+
out.text(HELP(out.C));
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
for (const f of flags) {
|
|
81
|
+
if (f === '--force') continue;
|
|
82
|
+
err.fail('jos init: unknown option ' + f + (f === '--pipeline' ? ' (removed in 5.0.0: init writes atoms; jos start writes a flow to copy)' : ''));
|
|
83
|
+
return 64;
|
|
84
|
+
}
|
|
85
|
+
if (files.length !== 1) {
|
|
86
|
+
err.fail('jos init: takes exactly one name');
|
|
87
|
+
err.hint('usage: jos init <name> [--force]');
|
|
88
|
+
return 64;
|
|
89
|
+
}
|
|
90
|
+
const t = target(files[0]);
|
|
91
|
+
if (!NAME.test(t.id) || t.id.length > 128) {
|
|
92
|
+
err.fail('jos init: invalid name ' + JSON.stringify(t.id));
|
|
93
|
+
err.hint('the id is the file name without .jos: lowercase letter first, then [a-z0-9._-], at most 128');
|
|
94
|
+
return 64;
|
|
95
|
+
}
|
|
96
|
+
const dir = path.dirname(t.file);
|
|
97
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
98
|
+
err.fail('jos init: ' + path.dirname(t.shown) + ' is not an existing directory');
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
if (fs.existsSync(t.file) && !flags.includes('--force')) {
|
|
102
|
+
err.fail('jos init: ' + t.shown + ' exists', '(use --force to overwrite)');
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
137
105
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
106
|
+
let k;
|
|
107
|
+
try { k = kernel.locate(); } catch (e) { return kernel.fail(e); }
|
|
108
|
+
const doc = template(t.id);
|
|
109
|
+
const v = k.core.validate(doc);
|
|
110
|
+
if (!v.valid) throw new Error('jos init: the template is not valid for jos-core ' + k.version + ': ' + v.errors.join('; '));
|
|
111
|
+
|
|
112
|
+
const tmp = t.file + '.tmp';
|
|
113
|
+
try {
|
|
114
|
+
fs.writeFileSync(tmp, JSON.stringify(doc, null, 2) + '\n');
|
|
115
|
+
fs.renameSync(tmp, t.file);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// the environment refused the write (EACCES, EROFS, ENOSPC...): an error, not a host bug
|
|
118
|
+
try { fs.rmSync(tmp, { force: true }); } catch (e2) { /* nothing to clean */ }
|
|
119
|
+
err.fail('jos init: cannot write ' + t.shown + ': ' + (e && e.code ? e.code : e.message));
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
153
122
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
sha256: sha256
|
|
160
|
-
};
|
|
161
|
-
fs.appendFileSync(path.join(runDir, 'events.jsonl'), JSON.stringify(event) + '\n');
|
|
123
|
+
err.header('JOS INIT', 'jos-core ' + k.version);
|
|
124
|
+
err.ok('Written: ' + t.shown, `(2.0.0 atom, id ${t.id}, ${v.artifact})`);
|
|
125
|
+
err.hint('Next: jos run ' + ui.shellArg(t.shown));
|
|
126
|
+
err.line();
|
|
127
|
+
return 0;
|
|
162
128
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOS LEDGER - `jos ledger verify <run_ | file>`: checks the hash chain of a run's ledger with jos-core
|
|
3
|
+
* (Ledger 0.1.0 §2): every entry hash recomputed, seq, prev and run continuous, the run ended with its `end` entry.
|
|
4
|
+
* Read-only. Exit codes: 0 verifies, 1 does not verify, 2 no ledger or unreadable, 5 no kernel, 64 usage.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
const kernel = require('../kernel');
|
|
8
|
+
const ledgers = require('../ledgers');
|
|
9
|
+
const ui = require('../ui');
|
|
10
|
+
const { RULE } = ui;
|
|
11
|
+
const { parse, problem } = require('../args');
|
|
12
|
+
|
|
13
|
+
const USAGE = 'usage: jos ledger verify <run_id | ledger.jsonl | run folder> [--json]';
|
|
14
|
+
|
|
15
|
+
const HELP = (C) => `
|
|
16
|
+
${C.purple}${RULE}${C.reset}
|
|
17
|
+
${C.bold}JOS LEDGER${C.reset} // Verify the ledger of a run with jos-core
|
|
18
|
+
${C.purple}${RULE}${C.reset}
|
|
19
|
+
|
|
20
|
+
${C.bold}Usage:${C.reset}
|
|
21
|
+
jos ledger verify <run_id | ledger.jsonl | run folder> [--json]
|
|
22
|
+
|
|
23
|
+
A run id (run_...) is looked up in .jos/replays/ of the folders above the current
|
|
24
|
+
directory, then in ~/.jos/runs/. Every entry's hash is recomputed over its canonical
|
|
25
|
+
form (RFC 8785); seq, prev and run must be continuous and the run must have ended.
|
|
26
|
+
Nothing is written.
|
|
27
|
+
|
|
28
|
+
${C.bold}Options:${C.reset}
|
|
29
|
+
--json Print the verification result as JSON
|
|
30
|
+
|
|
31
|
+
${C.bold}Exit codes:${C.reset}
|
|
32
|
+
0 verifies, 1 does not verify, 2 no ledger or unreadable, 5 no kernel, 64 usage
|
|
33
|
+
|
|
34
|
+
${C.bold}See also:${C.reset}
|
|
35
|
+
jos runs list the runs in ~/.jos/runs
|
|
36
|
+
jos meter <run_id> tokens, cost and time from a verified ledger
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
exports.execute = async (args, josHome) => {
|
|
40
|
+
const err = ui.printer(process.stderr);
|
|
41
|
+
const { opts, files, help, error } = parse(args, { '--json': false });
|
|
42
|
+
if (help) {
|
|
43
|
+
const out = ui.printer(process.stdout);
|
|
44
|
+
out.text(HELP(out.C));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
if (error || files[0] !== 'verify' || files.length !== 2) {
|
|
48
|
+
err.fail('jos ledger: ' + (error ? problem(error) : 'takes the subcommand verify and one run'));
|
|
49
|
+
err.hint(USAGE);
|
|
50
|
+
return 64;
|
|
51
|
+
}
|
|
52
|
+
const ref = files[1];
|
|
53
|
+
|
|
54
|
+
let core;
|
|
55
|
+
try { core = kernel.need(kernel.locate(), ['ledger.find', 'ledger.read', 'ledger.verify']); } catch (e) { return kernel.fail(e); }
|
|
56
|
+
const where = ledgers.locate(core, ref, josHome);
|
|
57
|
+
if (where.error === 'usage') {
|
|
58
|
+
err.fail('jos ledger: ' + ref + ' is not a run id (run_...) nor a ledger file');
|
|
59
|
+
err.hint(USAGE);
|
|
60
|
+
return 64;
|
|
61
|
+
}
|
|
62
|
+
if (where.error === 'missing') {
|
|
63
|
+
ledgers.explainMissing(err, 'ledger', ref, josHome);
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
const r = ledgers.read(core, where.file);
|
|
67
|
+
if (r.errors) {
|
|
68
|
+
err.fail('jos ledger: ' + where.file + ': ' + r.errors.join('; '));
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
const v = Object.assign(core.ledger.verify(r.lines, {}), { file: where.file });
|
|
72
|
+
if (opts['--json']) {
|
|
73
|
+
ui.json(process.stdout, v);
|
|
74
|
+
return v.ok ? 0 : 1;
|
|
75
|
+
}
|
|
76
|
+
const out = ui.printer(process.stdout);
|
|
77
|
+
out.header('JOS LEDGER', 'verify');
|
|
78
|
+
out.field('File', v.file);
|
|
79
|
+
if (v.ok) {
|
|
80
|
+
out.field('Run', v.run);
|
|
81
|
+
out.field('Entries', String(v.count));
|
|
82
|
+
out.field('Head', v.head);
|
|
83
|
+
out.line();
|
|
84
|
+
out.ok(out.C.bold + 'LEDGER VERIFIED', '(every hash recomputed, chain continuous, run ended)');
|
|
85
|
+
out.line();
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
out.field('Reason', v.reason);
|
|
89
|
+
out.field('First bad entry', String(v.first_bad_seq));
|
|
90
|
+
out.line();
|
|
91
|
+
out.fail(out.C.bold + 'LEDGER DOES NOT VERIFY');
|
|
92
|
+
out.line();
|
|
93
|
+
return 1;
|
|
94
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOS METER - `jos meter <run_ | file>`: the metering view of a run (Ledger 0.1.0 §5) computed by jos-core from the
|
|
3
|
+
* run's ledger, only when that ledger verifies: attempts, time, tokens, cost, approvals. A meter that no engine
|
|
4
|
+
* reported is "not reported", never zero. Read-only.
|
|
5
|
+
* Exit codes: 0 metered, 1 the ledger does not verify, 2 no ledger or unreadable, 5 no kernel, 64 usage.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
const kernel = require('../kernel');
|
|
9
|
+
const ledgers = require('../ledgers');
|
|
10
|
+
const ui = require('../ui');
|
|
11
|
+
const { RULE } = ui;
|
|
12
|
+
const { parse, problem } = require('../args');
|
|
13
|
+
|
|
14
|
+
const USAGE = 'usage: jos meter <run_id | ledger.jsonl | run folder> [--json]';
|
|
15
|
+
|
|
16
|
+
const HELP = (C) => `
|
|
17
|
+
${C.purple}${RULE}${C.reset}
|
|
18
|
+
${C.bold}JOS METER${C.reset} // Tokens, cost and time of a run, from its verified ledger
|
|
19
|
+
${C.purple}${RULE}${C.reset}
|
|
20
|
+
|
|
21
|
+
${C.bold}Usage:${C.reset}
|
|
22
|
+
jos meter <run_id | ledger.jsonl | run folder> [--json]
|
|
23
|
+
|
|
24
|
+
The ledger is found like \`jos ledger verify\` finds it, and must verify: the numbers
|
|
25
|
+
come only from its receipts. Tokens and cost appear when an engine reported them;
|
|
26
|
+
the shell engine reports neither, so they read "not reported" (never 0).
|
|
27
|
+
|
|
28
|
+
${C.bold}Options:${C.reset}
|
|
29
|
+
--json Print the meter as JSON (null for a meter no engine reported)
|
|
30
|
+
|
|
31
|
+
${C.bold}Exit codes:${C.reset}
|
|
32
|
+
0 metered, 1 the ledger does not verify, 2 no ledger or unreadable, 5 no kernel, 64 usage
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const shown = (v, f) => (v === null ? 'not reported' : f(v));
|
|
36
|
+
|
|
37
|
+
exports.execute = async (args, josHome) => {
|
|
38
|
+
const err = ui.printer(process.stderr);
|
|
39
|
+
const { opts, files, help, error } = parse(args, { '--json': false });
|
|
40
|
+
if (help) {
|
|
41
|
+
const out = ui.printer(process.stdout);
|
|
42
|
+
out.text(HELP(out.C));
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
if (error || files.length !== 1) {
|
|
46
|
+
err.fail('jos meter: ' + (error ? problem(error) : 'takes exactly one run'));
|
|
47
|
+
err.hint(USAGE);
|
|
48
|
+
return 64;
|
|
49
|
+
}
|
|
50
|
+
const ref = files[0];
|
|
51
|
+
|
|
52
|
+
let core;
|
|
53
|
+
try { core = kernel.need(kernel.locate(), ['ledger.find', 'ledger.read', 'ledger.meter']); } catch (e) { return kernel.fail(e); }
|
|
54
|
+
const where = ledgers.locate(core, ref, josHome);
|
|
55
|
+
if (where.error === 'usage') {
|
|
56
|
+
err.fail('jos meter: ' + ref + ' is not a run id (run_...) nor a ledger file');
|
|
57
|
+
err.hint(USAGE);
|
|
58
|
+
return 64;
|
|
59
|
+
}
|
|
60
|
+
if (where.error === 'missing') {
|
|
61
|
+
ledgers.explainMissing(err, 'meter', ref, josHome);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
const r = ledgers.read(core, where.file);
|
|
65
|
+
if (r.errors) {
|
|
66
|
+
err.fail('jos meter: ' + where.file + ': ' + r.errors.join('; '));
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
let m;
|
|
70
|
+
try { m = core.ledger.meter(r.lines); } catch (e) {
|
|
71
|
+
if (!(e instanceof core.JosError)) throw e;
|
|
72
|
+
err.fail('jos meter: the ledger of ' + ref + ' does not verify; meter reads only a verified ledger');
|
|
73
|
+
err.hint('see why: jos ledger verify ' + ui.shellArg(ref));
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (opts['--json']) {
|
|
77
|
+
ui.json(process.stdout, Object.assign({}, m, { file: where.file }));
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const out = ui.printer(process.stdout);
|
|
81
|
+
out.header('JOS METER', m.run);
|
|
82
|
+
out.field('Ledger', where.file + ' (verified)');
|
|
83
|
+
out.field('Code', m.code === null ? 'none (the run has no end entry)' : String(m.code));
|
|
84
|
+
out.field('Steps', m.steps.length ? m.steps.join(', ') : 'none');
|
|
85
|
+
out.field('Engines', m.engines.length ? m.engines.join(', ') : 'none');
|
|
86
|
+
out.field('Attempts', String(m.attempts));
|
|
87
|
+
out.field('Time', shown(m.duration_ms, (v) => v + ' ms (sum of the receipts)'));
|
|
88
|
+
out.field('Tokens in', shown(m.tokens_in, String));
|
|
89
|
+
out.field('Tokens out', shown(m.tokens_out, String));
|
|
90
|
+
out.field('Cost', shown(m.cost_usd, (v) => v + ' USD'));
|
|
91
|
+
out.field('Approvals', String(m.approvals));
|
|
92
|
+
out.field('Denials', String(m.denials));
|
|
93
|
+
out.field('Cache hits', String(m.cache_hits));
|
|
94
|
+
out.line();
|
|
95
|
+
return 0;
|
|
96
|
+
};
|