@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.
Files changed (59) hide show
  1. package/LICENSE +208 -21
  2. package/PROMISES.md +52 -0
  3. package/README.md +294 -146
  4. package/SECURITY-MCP.md +117 -0
  5. package/SECURITY-SECRETS.md +108 -0
  6. package/STYLE.md +23 -0
  7. package/bin/jos +139 -67
  8. package/bin/jos.cmd +5 -0
  9. package/examples/01-hello.jos +44 -0
  10. package/examples/02-criteria.jos +54 -0
  11. package/examples/03-flow-gate.jos +83 -0
  12. package/examples/04-use-template/README.md +22 -0
  13. package/examples/04-use-template/lib/greeting.jos +56 -0
  14. package/examples/04-use-template/use.jos +64 -0
  15. package/examples/05-sealed/.jos/manifest.jos +53 -0
  16. package/examples/05-sealed/README.md +25 -0
  17. package/examples/06-requires.jos +42 -0
  18. package/examples/07-sandbox.jos +39 -0
  19. package/examples/08-ids-ledger-meter.md +26 -0
  20. package/examples/10-ollama.jos +47 -0
  21. package/examples/11-openai.jos +47 -0
  22. package/examples/12-gemini.jos +47 -0
  23. package/examples/13-anthropic.jos +47 -0
  24. package/examples/14-serve.jos +88 -0
  25. package/examples/README.md +105 -0
  26. package/examples/hello.jos +47 -0
  27. package/package.json +31 -9
  28. package/promises.json +894 -0
  29. package/src/args.js +47 -0
  30. package/src/commands/_frozen/add.README.md +1 -0
  31. package/src/commands/_frozen/get.README.md +1 -0
  32. package/src/commands/_frozen/llm.README.md +1 -0
  33. package/src/commands/_frozen/prompts.README.md +1 -0
  34. package/src/commands/_frozen/repo.README.md +1 -0
  35. package/src/commands/_frozen/serve-explorer.README.md +1 -0
  36. package/src/commands/ids.js +101 -0
  37. package/src/commands/init.js +113 -147
  38. package/src/commands/ledger.js +94 -0
  39. package/src/commands/meter.js +96 -0
  40. package/src/commands/migrate.js +214 -0
  41. package/src/commands/overview.js +118 -0
  42. package/src/commands/run.js +164 -214
  43. package/src/commands/runs.js +102 -0
  44. package/src/commands/secrets.js +695 -115
  45. package/src/commands/start.js +210 -0
  46. package/src/commands/validate.js +153 -165
  47. package/src/commands/verify.js +56 -0
  48. package/src/commands/version.js +23 -0
  49. package/src/intention.js +114 -0
  50. package/src/kernel.js +69 -0
  51. package/src/ledgers.js +57 -0
  52. package/src/mcp-guide.js +138 -0
  53. package/src/mcp.js +734 -0
  54. package/src/serve.js +38 -770
  55. package/src/ui.js +98 -0
  56. package/src/commands/add.js +0 -145
  57. package/src/commands/get.js +0 -245
  58. package/src/commands/repo.js +0 -139
  59. 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
+ };
@@ -1,162 +1,128 @@
1
1
  /**
2
- * JOS INIT Command - Create new .jos artifacts
3
- * Creates a minimal valid .jos with detached integrity manifest
4
- * Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)
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 crypto = require('crypto');
10
-
11
- const C = {
12
- reset: '\x1b[0m', bold: '\x1b[1m',
13
- purple: '\x1b[38;5;135m', cyan: '\x1b[38;5;51m',
14
- green: '\x1b[38;5;78m', gray: '\x1b[38;5;245m'
15
- };
16
-
17
- exports.execute = async (args, home) => {
18
- const name = args[0] || 'new-artifact';
19
- const type = args.includes('--pipeline') ? 'pipeline' : 'atom';
20
- const filename = `${name}.jos`;
21
-
22
- if (args.includes('--help') || args.includes('-h')) {
23
- console.log(`
24
- ${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
25
- ${C.bold}JOS INIT${C.reset} // Create new .jos artifacts
26
- ${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
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> [options]
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
- console.log(`\n${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
42
- console.log(`${C.bold}JOS INIT${C.reset} // ${C.gray}Creating ${type} artifact${C.reset}`);
43
- console.log(`${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`);
44
-
45
- // Create artifact with full JOSFOXAI + MAGIC compliance
46
- const template = {
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
- // MAGIC Kernel
95
- "meta": {
96
- "version": "1.0.0",
97
- "type": type,
98
- "name": name,
99
- "provider": "local",
100
- "author": "JOS Developer",
101
- "canonical_version": "Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)"
102
- },
103
- "artifacts": {
104
- "description": `Auto-generated ${type} artifact. Update this description.`,
105
- "outputs": []
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
- "guardrails": {
108
- "avoid": [],
109
- "max_retries": 1
58
+ guardrails: {
59
+ avoid: ['rm -rf', 'curl', 'wget'],
60
+ limit: { seconds: 30, output_bytes: 65536, retries: 0 },
110
61
  },
111
- "intention": {
112
- "objective": "Define your objective here",
113
- "success_criteria": "Define success criteria here"
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
- const manifestPath = filename + '.sig.json';
139
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
140
- console.log(`${C.green}✓ Created:${C.reset} ${manifestPath}`);
141
- console.log(`${C.gray} SHA256: ${sha256}${C.reset}`);
142
-
143
- console.log(`
144
- ${C.cyan}Next steps:${C.reset}
145
- 1. Edit ${filename} to define your tasks
146
- 2. Run: ${C.purple}jos validate ${filename}${C.reset}
147
- 3. Run: ${C.purple}jos run ${filename}${C.reset}
148
- `);
149
-
150
- // Log to witness
151
- const runDir = path.join(home, 'runs', 'init-' + new Date().toISOString().replace(/[:.]/g, '-'));
152
- if (!fs.existsSync(runDir)) fs.mkdirSync(runDir, { recursive: true });
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
- const event = {
155
- timestamp: new Date().toISOString(),
156
- type: 'init:created',
157
- artifact: filename,
158
- manifest: manifestPath,
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
+ };