@josfox/jos 4.1.0 → 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 (64) hide show
  1. package/LICENSE +208 -21
  2. package/PROMISES.md +52 -0
  3. package/README.md +334 -138
  4. package/SECURITY-MCP.md +117 -0
  5. package/SECURITY-SECRETS.md +108 -0
  6. package/STYLE.md +23 -0
  7. package/bin/jos +170 -160
  8. package/examples/01-hello.jos +44 -0
  9. package/examples/02-criteria.jos +54 -0
  10. package/examples/03-flow-gate.jos +83 -0
  11. package/examples/04-use-template/README.md +22 -0
  12. package/examples/04-use-template/lib/greeting.jos +56 -0
  13. package/examples/04-use-template/use.jos +64 -0
  14. package/examples/05-sealed/.jos/manifest.jos +53 -0
  15. package/examples/05-sealed/README.md +25 -0
  16. package/examples/06-requires.jos +42 -0
  17. package/examples/07-sandbox.jos +39 -0
  18. package/examples/08-ids-ledger-meter.md +26 -0
  19. package/examples/10-ollama.jos +47 -0
  20. package/examples/11-openai.jos +47 -0
  21. package/examples/12-gemini.jos +47 -0
  22. package/examples/13-anthropic.jos +47 -0
  23. package/examples/14-serve.jos +88 -0
  24. package/examples/README.md +105 -0
  25. package/examples/hello.jos +47 -0
  26. package/package.json +57 -40
  27. package/promises.json +894 -0
  28. package/src/args.js +47 -0
  29. package/src/commands/_frozen/add.README.md +1 -0
  30. package/src/commands/_frozen/get.README.md +1 -0
  31. package/src/commands/_frozen/llm.README.md +1 -0
  32. package/src/commands/_frozen/prompts.README.md +1 -0
  33. package/src/commands/_frozen/repo.README.md +1 -0
  34. package/src/commands/_frozen/serve-explorer.README.md +1 -0
  35. package/src/commands/ids.js +101 -0
  36. package/src/commands/init.js +128 -162
  37. package/src/commands/ledger.js +94 -0
  38. package/src/commands/meter.js +96 -0
  39. package/src/commands/migrate.js +214 -0
  40. package/src/commands/overview.js +118 -0
  41. package/src/commands/run.js +194 -244
  42. package/src/commands/runs.js +102 -0
  43. package/src/commands/secrets.js +717 -137
  44. package/src/commands/start.js +210 -0
  45. package/src/commands/validate.js +178 -194
  46. package/src/commands/verify.js +56 -98
  47. package/src/commands/version.js +23 -0
  48. package/src/intention.js +114 -0
  49. package/src/kernel.js +69 -0
  50. package/src/ledgers.js +57 -0
  51. package/src/mcp-guide.js +138 -0
  52. package/src/mcp.js +734 -0
  53. package/src/serve.js +48 -781
  54. package/src/ui.js +98 -0
  55. package/src/adapters/index.js +0 -16
  56. package/src/adapters/native.js +0 -41
  57. package/src/adapters/ollama.js +0 -140
  58. package/src/commands/add.js +0 -145
  59. package/src/commands/get.js +0 -245
  60. package/src/commands/llm.js +0 -159
  61. package/src/commands/prompts.js +0 -229
  62. package/src/commands/repo.js +0 -139
  63. package/src/utils/crypto.js +0 -41
  64. package/src/utils/request.js +0 -41
@@ -1,98 +1,56 @@
1
- /**
2
- * JOS VERIFY Command - Self-Verification Suite
3
- * Implements "Dogfooding" strategy
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const { spawnSync, execSync } = require('child_process');
9
-
10
- const C = {
11
- reset: '\x1b[0m', bold: '\x1b[1m',
12
- green: '\x1b[38;5;78m', red: '\x1b[38;5;196m',
13
- cyan: '\x1b[38;5;51m', gray: '\x1b[38;5;245m',
14
- yellow: '\x1b[38;5;220m'
15
- };
16
-
17
- exports.execute = async (args, home) => {
18
- console.log(`\n${C.cyan}${C.bold}🦊 JOS Self-Verification${C.reset}\n`);
19
-
20
- const binDir = path.join(__dirname, '..', '..', 'bin');
21
- const binJos = path.join(binDir, 'jos');
22
-
23
- // 1. PATH Injection (Crucial for Windows)
24
- // We add the bin directory to the front of PATH so 'jos' resolves to our binary
25
- const env = { ...process.env, CI: 'true' };
26
- env.PATH = process.platform === 'win32'
27
- ? `${binDir};${process.env.PATH}`
28
- : `${binDir}:${process.env.PATH}`;
29
-
30
- let failed = false;
31
-
32
- // 2. Dogfooding: Run verification.jos
33
- console.log(`${C.bold}1. Kernel Smoke Tests (verification.jos)${C.reset}`);
34
- const verifyArtifact = path.join(__dirname, '..', '..', 'artifacts', 'verification.jos');
35
-
36
- if (!fs.existsSync(verifyArtifact)) {
37
- console.error(`${C.red}✖ Internal Error: verification.jos not found at ${verifyArtifact}${C.reset}`);
38
- process.exit(1);
39
- }
40
-
41
- try {
42
- // We defer to the 'run' command logic by spawning a child process
43
- // This ensures identical execution environment to a real user run
44
- const result = spawnSync(process.execPath, [binJos, 'run', verifyArtifact], {
45
- encoding: 'utf8',
46
- env,
47
- stdio: 'inherit'
48
- });
49
-
50
- if (result.status !== 0) {
51
- console.error(`\n${C.red}✖ Smoke tests failed (Exit Code: ${result.status})${C.reset}`);
52
- failed = true;
53
- } else {
54
- console.log(`${C.green}✓ Smoke tests passed${C.reset}`);
55
- }
56
-
57
- } catch (e) {
58
- console.error(`${C.red}✖ Execution failed: ${e.message}${C.reset}`);
59
- failed = true;
60
- }
61
-
62
- // 3. Functional Test (Init + Validate)
63
- console.log(`\n${C.bold}2. Functional Lifecycle (Init + Validate)${C.reset}`);
64
- const TEST_ARTIFACT = 'native-test';
65
- const TEST_FILE = `${TEST_ARTIFACT}.jos`;
66
-
67
- // Cleanup
68
- if (fs.existsSync(TEST_FILE)) fs.unlinkSync(TEST_FILE);
69
-
70
- try {
71
- console.log(`${C.gray}→ Initializing ${TEST_ARTIFACT}...${C.reset}`);
72
- execSync(`"${process.execPath}" "${binJos}" init ${TEST_ARTIFACT} --pipeline`, { env, stdio: 'pipe' });
73
-
74
- if (!fs.existsSync(TEST_FILE)) {
75
- throw new Error('Artifact file not created');
76
- }
77
-
78
- console.log(`${C.gray}→ Validating artifact...${C.reset}`);
79
- execSync(`"${process.execPath}" "${binJos}" validate ${TEST_FILE}`, { env, stdio: 'pipe' });
80
-
81
- console.log(`${C.green}✓ Lifecycle tests passed${C.reset}`);
82
-
83
- } catch (e) {
84
- console.error(`${C.red}✖ Functional test failed: ${e.message}${C.reset}`);
85
- failed = true;
86
- } finally {
87
- if (fs.existsSync(TEST_FILE)) fs.unlinkSync(TEST_FILE);
88
- }
89
-
90
- console.log(`\n${C.gray}----------------------------------------${C.reset}`);
91
- if (failed) {
92
- console.log(`${C.red}${C.bold}❌ VERIFICATION FAILED${C.reset}`);
93
- process.exit(1);
94
- } else {
95
- console.log(`${C.green}${C.bold}✅ SYSTEM HEALTHY${C.reset}`);
96
- process.exit(0);
97
- }
98
- };
1
+ /**
2
+ * JOS VERIFY - Self-check: runs the embedded examples/hello.jos through jos-core
3
+ * Exit 0 when the kernel returns ok, 1 otherwise, 5 when no kernel is found.
4
+ *
5
+ * The check is hermetic: hello.jos is copied to a fresh temporary directory, which is also the HOME the kernel sees, so
6
+ * no `.jos/` chain of the user or of the folder where jos is installed applies, and the ledger of the run lands in that
7
+ * directory. The directory is removed at the end: verify leaves nothing in ~/.jos or anywhere else.
8
+ */
9
+ 'use strict';
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const pkg = require('../../package.json');
14
+ const kernel = require('../kernel');
15
+ const ui = require('../ui');
16
+
17
+ const HELLO = path.join(__dirname, '..', '..', 'examples', 'hello.jos');
18
+
19
+ exports.execute = async () => {
20
+ const out = ui.printer(process.stdout);
21
+ const { C } = out;
22
+
23
+ let k;
24
+ try { k = kernel.locate(); } catch (e) { return kernel.fail(e); }
25
+ const core = k.core;
26
+ out.header('JOS VERIFY', `jos ${pkg.version} | jos-core ${k.version} (${k.source})`);
27
+
28
+ out.line(`${C.bold}1. Kernel smoke test (examples/hello.jos)${C.reset}`);
29
+ let failed = false;
30
+ let tmp = null;
31
+ try {
32
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'jos-verify-'));
33
+ const file = path.join(tmp, 'hello.jos');
34
+ fs.copyFileSync(HELLO, file);
35
+ // read once and run with the octets, as jos run does (jos-core 2.0.0-rc.8 verifies a 0.0.7 integrity_ref on them)
36
+ const { doc, bytes } = core.loadFileBytes(file);
37
+ const r = await core.run(doc, { cwd: tmp, dir: tmp, home: tmp, source: bytes, env: process.env });
38
+ if (r.ok === true && r.code === 0) {
39
+ out.ok('ok', `${r.artifact} (${r.duration_ms} ms)`);
40
+ } else {
41
+ out.fail(`not ok (code ${r.code})`, r.errors ? r.errors.join('; ') : '');
42
+ failed = true;
43
+ }
44
+ } catch (e) {
45
+ out.fail(e.message);
46
+ failed = true;
47
+ } finally {
48
+ if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
49
+ }
50
+
51
+ out.line();
52
+ if (failed) out.fail(`${C.bold}VERIFICATION FAILED`);
53
+ else out.ok(`${C.bold}SYSTEM HEALTHY`);
54
+ out.line();
55
+ return failed ? 1 : 0;
56
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * JOS VERSION - host version and the jos-core kernel it found
3
+ * Two plain lines (scripts read them): `jos <version>` and `jos-core <version> (<source>: <file>)`; with color on a
4
+ * terminal the location is gray. Without a kernel the second line is `jos-core not found` and the exit code is 0.
5
+ */
6
+ 'use strict';
7
+ const pkg = require('../../package.json');
8
+ const kernel = require('../kernel');
9
+ const ui = require('../ui');
10
+
11
+ exports.execute = async () => {
12
+ const out = ui.printer(process.stdout);
13
+ const { C } = out;
14
+ out.line('jos ' + pkg.version);
15
+ try {
16
+ const k = kernel.locate();
17
+ out.line('jos-core ' + k.version + ` ${C.gray}(` + k.source + ': ' + k.file + `)${C.reset}`);
18
+ } catch (e) {
19
+ if (!(e instanceof kernel.KernelNotFound)) throw e;
20
+ out.line('jos-core not found');
21
+ }
22
+ return 0;
23
+ };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+ // The intention of a document and the verdict of a run, in words, for `jos run` and `jos validate` (text mode only;
3
+ // `--json` is the kernel's result unchanged). The intention comes from the document as jos-core normalized it
4
+ // (intention.objective, intention.success_criteria, guardrails.avoid and guardrails.limit); the verdict comes only
5
+ // from the receipts the kernel returned (criteria[].check / verdict / detail): the host evaluates nothing itself.
6
+
7
+ const ONE_LINE = 160;
8
+
9
+ const isObj = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
10
+ const q = (s) => JSON.stringify(s);
11
+
12
+ function oneLine(s) {
13
+ const t = s.replace(/\s+/g, ' ').trim();
14
+ return t.length > ONE_LINE ? t.slice(0, ONE_LINE - 1) + '…' : t;
15
+ }
16
+
17
+ // A declared success criterion as a short phrase: "exit 0", 'stdout contains "x"', "all of (exit 0; stderr is empty)".
18
+ // Criteria come from a document jos-core validated, so each object has the one key its schema allows.
19
+ const short = (h) => h.replace(/^sha256:/, '').slice(0, 12) + '…';
20
+ function describe(c) {
21
+ if (typeof c === 'string') return oneLine(c) + ' (descriptive)';
22
+ const k = Object.keys(c)[0];
23
+ const v = c[k];
24
+ switch (k) {
25
+ case 'exit': return 'exit ' + v;
26
+ case 'stdout_contains': return 'stdout contains ' + q(v);
27
+ case 'stdout_matches': return 'stdout matches /' + v + '/';
28
+ case 'stderr_empty': return 'stderr is empty';
29
+ case 'file_exists': return 'file ' + q(v) + ' exists';
30
+ case 'file_sha256': return 'file ' + q(v.path) + ' has sha256 ' + short(v.hash);
31
+ case 'sha256': return 'output has sha256 ' + short(v);
32
+ case 'json_schema': return 'output validates against a JSON Schema';
33
+ case 'judge': return 'a judge engine grades it: ' + oneLine(v.rubric);
34
+ case 'all': case 'any': return k + ' of (' + v.map(describe).join('; ') + ')';
35
+ case 'not': return 'not (' + describe(v) + ')';
36
+ default: return k + ' ' + q(v);
37
+ }
38
+ }
39
+
40
+ const LIMIT_WORDS = { seconds: ' s', output_bytes: ' output bytes per stream', tokens: ' tokens', retries: ' retries' };
41
+ function limitText(limit) {
42
+ return Object.keys(limit).map((k) => {
43
+ if (k === 'cost_usd') return limit[k] + ' USD';
44
+ if (Object.prototype.hasOwnProperty.call(LIMIT_WORDS, k)) return limit[k] + LIMIT_WORDS[k];
45
+ return k + ' ' + q(limit[k]);
46
+ }).join(', ');
47
+ }
48
+
49
+ // "Intention:", "Done when:", and when declared "Avoid:" and "Limit:", for the normalized document `doc`.
50
+ function intention(p, doc) {
51
+ const { C } = p;
52
+ const it = isObj(doc.intention) ? doc.intention : {};
53
+ const g = isObj(doc.guardrails) ? doc.guardrails : {};
54
+ p.line();
55
+ p.line(`${C.bold}Intention:${C.reset} ` + (typeof it.objective === 'string' && it.objective.trim() ? oneLine(it.objective) : `${C.gray}not declared${C.reset}`));
56
+ const criteria = it.success_criteria === undefined ? [] : [].concat(it.success_criteria);
57
+ if (!criteria.length) p.line(`${C.bold}Done when:${C.reset} ${C.gray}not declared${C.reset}`);
58
+ else {
59
+ p.line(`${C.bold}Done when:${C.reset}`);
60
+ for (const c of criteria) p.item(describe(c));
61
+ }
62
+ if (Array.isArray(g.avoid) && g.avoid.length) p.line(`${C.bold}Avoid:${C.reset} ` + g.avoid.map(q).join(', '));
63
+ if (isObj(g.limit) && Object.keys(g.limit).length) p.line(`${C.bold}Limit:${C.reset} ` + limitText(g.limit));
64
+ }
65
+
66
+ // "Inferred:": the `inferred` section of jos-core (2.0.0-rc.8) for a valid document, one line per JSON Pointer in the
67
+ // kernel's order: what the kernel applies that the document does not write (value and source, SPEC section or jos-core
68
+ // default, and its note), and, flagged with ⚠, what the document does not declare.
69
+ function inferred(p, section) {
70
+ const { C } = p;
71
+ const keys = Object.keys(section);
72
+ p.line();
73
+ if (!keys.length) {
74
+ p.line(`${C.bold}Inferred:${C.reset} ${C.gray}nothing (the document writes every member jos-core applies)${C.reset}`);
75
+ return;
76
+ }
77
+ p.line(`${C.bold}Inferred:${C.reset} ${C.gray}(applied by jos-core, not written in the document)${C.reset}`);
78
+ for (const k of keys) {
79
+ const e = section[k];
80
+ const note = e.note ? ` ${C.gray}— ${oneLine(e.note)}${C.reset}` : '';
81
+ if (e.source === 'not declared') p.line(` ${C.yellow}⚠ ${k}: not declared${C.reset}` + note);
82
+ else p.item(k + (Object.prototype.hasOwnProperty.call(e, 'value') ? ' = ' + oneLine(JSON.stringify(e.value)) : '') + ` ${C.gray}(${e.source})${C.reset}` + note);
83
+ }
84
+ }
85
+
86
+ // One line per criterion verdict of a receipt, indented by depth; combinators list their items below them.
87
+ function criteriaLines(p, list, depth) {
88
+ const { C } = p;
89
+ const pad = ' '.repeat(depth);
90
+ for (const c of list) {
91
+ const sign = c.verdict === 'pass' ? `${C.green}✓` : c.verdict === 'fail' ? `${C.red}✖` : `${C.gray}•`;
92
+ const note = c.verdict === 'descriptive' ? ' (descriptive, recorded only)' : '';
93
+ p.line(`${pad}${sign} ${c.check}${C.reset}${C.gray}: ${c.detail}${note}${C.reset}`);
94
+ if (Array.isArray(c.items)) criteriaLines(p, c.items, depth + 1);
95
+ }
96
+ }
97
+
98
+ // "Verdict:": per receipt (each step of a flow, then the flow itself) its code and the verdict of each criterion.
99
+ // The caller passes a result with at least one receipt.
100
+ function verdict(p, result) {
101
+ const { C } = p;
102
+ p.line();
103
+ p.line(`${C.bold}Verdict:${C.reset}`);
104
+ for (const r of result.receipts) {
105
+ const mark = r.ok ? `${C.green}✓` : `${C.red}✖`;
106
+ const label = r.flow ? r.step + ' (flow ' + r.flow + ')' : r.step;
107
+ p.line(` ${C.blue}[${r.attempt}] ${label}${C.reset} ${mark} code ${r.code}${C.reset}` + (r.reason ? ` ${C.gray}${r.reason}${C.reset}` : ''));
108
+ const list = r.criteria || [];
109
+ if (list.length) criteriaLines(p, list, 2);
110
+ else p.line(` ${C.gray}no criterion evaluated${C.reset}`);
111
+ }
112
+ }
113
+
114
+ module.exports = { describe, limitText, oneLine, intention, inferred, verdict };
package/src/kernel.js ADDED
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+ // Locates the jos-core reference kernel. No network.
3
+ // 1. the installed dependency @josfox/jos-core
4
+ // 2. ../../../kernel/js/jos-core.js (monorepo checkout)
5
+ // 3. otherwise a clear error; hosts map it to exit code 5 (SPEC-2.0 section 7.1).
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const ui = require('./ui');
10
+
11
+ const MONOREPO_PATH = path.resolve(__dirname, '..', '..', '..', 'kernel', 'js', 'jos-core.js');
12
+
13
+ class KernelNotFound extends Error {}
14
+
15
+ let cached = null;
16
+
17
+ function locate() {
18
+ if (cached) return cached;
19
+ let file = null;
20
+ let source = null;
21
+ try {
22
+ file = require.resolve('@josfox/jos-core');
23
+ source = 'dependency';
24
+ } catch (e) {
25
+ if (e.code !== 'MODULE_NOT_FOUND') throw e;
26
+ }
27
+ if (!file && fs.existsSync(MONOREPO_PATH)) {
28
+ file = MONOREPO_PATH;
29
+ source = 'monorepo';
30
+ }
31
+ if (!file) {
32
+ throw new KernelNotFound(
33
+ 'jos-core not found. Install @josfox/jos-core next to @josfox/jos, ' +
34
+ 'or run from a checkout that contains kernel/js/jos-core.js.');
35
+ }
36
+ let core;
37
+ try {
38
+ core = require(file);
39
+ } catch (e) {
40
+ throw new KernelNotFound('jos-core at ' + file + ' failed to load: ' + e.message);
41
+ }
42
+ for (const fn of ['validate', 'run', 'loadFile']) {
43
+ if (typeof core[fn] !== 'function') {
44
+ throw new KernelNotFound('jos-core at ' + file + ' does not export ' + fn + '()');
45
+ }
46
+ }
47
+ cached = { core, version: core.VERSION, file, source };
48
+ return cached;
49
+ }
50
+
51
+ // The kernel's core when it has every function in names ('validate', 'ledger.verify', ...); otherwise KernelNotFound
52
+ // naming the first one missing (an older jos-core), which a command turns into exit 5 with fail().
53
+ function need(k, names) {
54
+ for (const n of names) {
55
+ let v = k.core;
56
+ for (const part of n.split('.')) v = v === null || v === undefined ? undefined : v[part];
57
+ if (typeof v !== 'function') throw new KernelNotFound('jos-core ' + k.version + ' at ' + k.file + ' does not export ' + n + '()');
58
+ }
59
+ return k.core;
60
+ }
61
+
62
+ // Prints the error and returns exit code 5, for use as `return kernel.fail(e)`.
63
+ function fail(e) {
64
+ if (!(e instanceof KernelNotFound)) throw e;
65
+ ui.printer(process.stderr).fail('jos: ' + e.message);
66
+ return 5;
67
+ }
68
+
69
+ module.exports = { locate, need, fail, KernelNotFound, MONOREPO_PATH };
package/src/ledgers.js ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+ // Run ledgers for `jos ledger verify`, `jos meter` and `jos runs`: thin calls into the ledger API of jos-core
3
+ // (Ledger 0.1.0: find, read, verify, meter). Read-only: nothing here writes, creates ~/.jos or runs a document.
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // A run id as jos-core mints it (`run_` + a ULID); anything else must be the path of a ledger.
8
+ const RUN_ID = /^run_[0-9A-Za-z]{1,64}$/;
9
+
10
+ const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch (e) { return false; } };
11
+ const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } };
12
+
13
+ // Where the ledger of `ref` is: { file } for a ledger file, a run folder holding ledger.jsonl, or a run id that
14
+ // jos-core finds (.jos/replays/ of the folders above the current directory, then ~/.jos/runs/); { error: 'usage' }
15
+ // when ref is neither a path nor a run id; { error: 'missing' } when no ledger of that run exists.
16
+ function locate(core, ref, josHome) {
17
+ if (isFile(ref)) return { file: path.resolve(ref) };
18
+ if (isDir(ref) && isFile(path.join(ref, 'ledger.jsonl'))) return { file: path.resolve(ref, 'ledger.jsonl') };
19
+ if (!RUN_ID.test(ref)) return { error: 'usage' };
20
+ const file = core.ledger.find(ref, { home: path.dirname(josHome) });
21
+ return file === null ? { error: 'missing' } : { file };
22
+ }
23
+
24
+ // The lines of a ledger file, or { errors } when it cannot be read (a JosError of the kernel).
25
+ function read(core, file) {
26
+ try { return { lines: core.ledger.read(file) }; } catch (e) {
27
+ if (!(e instanceof core.JosError)) throw e;
28
+ return { errors: e.errors };
29
+ }
30
+ }
31
+
32
+ // What a listing shows of one run, from its ledger lines: whether the ledger verifies and, only when it does, when the
33
+ // run started, the document (the name on its top-level receipt, the last one, and the art_ it admitted), its exit code
34
+ // and its metered cost (null when no engine reported one). A ledger that does not verify says only why.
35
+ const str = (x) => (typeof x === 'string' ? x : null);
36
+ const obj = (x) => (x !== null && typeof x === 'object' ? x : {});
37
+ function summary(core, lines) {
38
+ const v = core.ledger.verify(lines, {});
39
+ if (!v.ok) return { ledger_ok: false, reason: v.reason, started: null, document: null, artifact: null, code: null, cost_usd: null };
40
+ const entries = lines.map((l) => JSON.parse(l));
41
+ const m = core.ledger.meter(lines);
42
+ const receipts = entries.filter((e) => e.kind === 'receipt');
43
+ const top = receipts.length ? obj(receipts[receipts.length - 1].body) : {};
44
+ return {
45
+ ledger_ok: true, reason: null, started: str(entries[0].ts), document: str(top.step),
46
+ artifact: str(obj(entries[entries.length - 1].body).artifact), code: typeof m.code === 'number' ? m.code : null, cost_usd: m.cost_usd,
47
+ };
48
+ }
49
+
50
+ // The text after a failed lookup, for the printer of a command.
51
+ function explainMissing(err, cmd, ref, josHome) {
52
+ err.fail('jos ' + cmd + ': no ledger for ' + ref);
53
+ err.hint('searched .jos/replays/ of the folders above ' + process.cwd() + ' and ' + path.join(josHome, 'runs'));
54
+ err.hint('list the runs with: jos runs');
55
+ }
56
+
57
+ module.exports = { RUN_ID, locate, read, summary, explainMissing };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * What a person reads about `jos serve --mcp`: the help screen (`jos serve --help`, stdout) and the notice the server
3
+ * prints on stderr when its stdin is a terminal (a person, not an MCP client). Both name the tools with one line each,
4
+ * what a client cannot do, and how to connect this very bin/jos, with its absolute path resolved at run time.
5
+ * Nothing here writes to the JSON-RPC channel; every line goes through a printer of ui.js (STYLE.md).
6
+ */
7
+ 'use strict';
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const ui = require('./ui');
12
+
13
+ const BIN = path.resolve(__dirname, '..', 'bin', 'jos');
14
+ const SECURITY = path.resolve(__dirname, '..', 'SECURITY-MCP.md');
15
+
16
+ // One line per tool of src/mcp.js (test/mcp.test.js checks that every tool has one and that no extra one is listed).
17
+ const USES = {
18
+ list_files: 'list the files and folders of a directory under the root (recursive to depth 3)',
19
+ read_file: 'read a regular file under the root (at most 10 MB)',
20
+ search_files: 'find files under the root by a glob on the name (* and ?; 100 matches, depth 5)',
21
+ read_markdown: 'read a Markdown file under the root, with its line count',
22
+ list_modules: 'list the modules in ~/.jos/modules and whether ~/.jos/modules.json enables them',
23
+ list_processes: 'list the runs recorded in ~/.jos/runs/*/report.json',
24
+ hot_reload: 'read ~/.jos/modules.json again and report the enabled and disabled modules',
25
+ write_file: 'create or overwrite a file under the root, never under ~/.jos',
26
+ run_command: 'run a program with an argv (no shell) in a folder under the root; each one shown on stderr',
27
+ };
28
+ const FLAG = { write_file: '--allow-write', run_command: '--allow-shell' };
29
+
30
+ // What --allow-shell grants, said the same way in the help screen, the notice and SECURITY-MCP.md (review OAI-RC6-05).
31
+ const SHELL_RISK = '--allow-shell grants local code execution with your user privileges: a program it runs can read ' +
32
+ 'your secrets (~/.jos/secrets, ~/.ssh, cloud keys) outside the MCP tools and their denials. Do not give it to an ' +
33
+ 'untrusted agent without an external sandbox (a container or a virtual machine).';
34
+
35
+ const CANNOT = [
36
+ 'reach a file outside the root (real paths, symlinks resolved, device and inode compared)',
37
+ 'read, list, search or write ~/.jos/secrets, nor any .master or vault.json under ~/.jos',
38
+ 'write a file without --allow-write, nor under ~/.jos with it',
39
+ 'run a program without --allow-shell (with it the program runs as you and could read ~/.jos/secrets)',
40
+ ];
41
+
42
+ // The absolute path of a `jos` on PATH that is this bin/jos (an npm install), or null.
43
+ function onPath(env) {
44
+ const self = fs.realpathSync(BIN);
45
+ for (const dir of String(env.PATH || '').split(path.delimiter)) {
46
+ if (!path.isAbsolute(dir)) continue;
47
+ const p = path.join(dir, 'jos');
48
+ try { if (fs.realpathSync(p) === self) return p; } catch (e) { /* not there */ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ // How to connect this server, read-only, with `root` as its root: Claude Code, then Claude Desktop / Cursor.
54
+ function connect(p, root, env) {
55
+ const { C } = p;
56
+ const args = [BIN, 'serve', '--mcp', '--root', root];
57
+ p.line(`${C.bold}Connect it${C.reset} ${C.gray}(read-only, root ${root})${C.reset}`);
58
+ const home = os.homedir();
59
+ if (home === root || home.startsWith(root.endsWith(path.sep) ? root : root + path.sep)) {
60
+ p.warn('This root is your home directory or contains it: put a project folder after --root before you copy these.');
61
+ }
62
+ p.line(` ${C.white}Claude Code:${C.reset}`);
63
+ p.cmd('claude mcp add jos -- node ' + args.map(ui.shellArg).join(' '));
64
+ const installed = onPath(env);
65
+ if (installed) p.cmd('claude mcp add jos -- jos ' + args.slice(1).map(ui.shellArg).join(' ') + `${C.reset} ${C.gray}(jos on PATH: ${installed})`);
66
+ p.cmd('claude mcp remove jos' + `${C.reset} ${C.gray}(to remove it)`);
67
+ p.line(` ${C.white}Claude Desktop${C.reset} ${C.gray}(claude_desktop_config.json)${C.reset} ${C.white}or Cursor${C.reset} ${C.gray}(.cursor/mcp.json):${C.reset}`);
68
+ const block = JSON.stringify({ mcpServers: { jos: { command: 'node', args } } }, null, 2);
69
+ for (const l of block.split('\n')) p.line(' ' + l);
70
+ p.hint('If the client cannot find node, put its absolute path in "command": ' + process.execPath);
71
+ p.hint('Add --allow-write or --allow-shell to the arguments only if the client needs them (SECURITY-MCP.md).');
72
+ }
73
+
74
+ function tools(p, names, marked) {
75
+ const w = Math.max(...names.map((n) => n.length));
76
+ for (const n of names) {
77
+ p.item(`${p.C.cyan}${n.padEnd(w)}${p.C.reset} ${USES[n]}` + (marked && FLAG[n] ? ` ${p.C.yellow}(only with ${FLAG[n]})${p.C.reset}` : ''));
78
+ }
79
+ }
80
+
81
+ // `jos serve --help`. names: every tool of src/mcp.js, the gated ones included; root: the root it would serve.
82
+ function help(stream, names, root, env) {
83
+ const p = ui.printer(stream);
84
+ const { C } = p;
85
+ p.header('JOS SERVE', 'MCP server over stdio (Model Context Protocol)');
86
+ p.text(`${C.bold}Usage:${C.reset}
87
+ jos serve --mcp [--root <dir>] [--allow-write] [--allow-shell]
88
+
89
+ ${C.bold}Options:${C.reset}
90
+ --mcp Start as MCP server (stdio JSON-RPC mode); read-only by default
91
+ --root <dir> The one directory the client may reach (default: the working
92
+ directory). Without it the server refuses to start in "/", in
93
+ your home directory or in any directory that contains it.
94
+ --allow-write Also offer write_file (inside the root, never under ~/.jos)
95
+ --allow-shell Also offer run_command: the client runs any program as you, with
96
+ an argv and no shell (announced on stderr; variables named like a
97
+ secret of the vault are dropped from its environment). This is
98
+ local code execution with your privileges: never for an
99
+ untrusted agent without an external sandbox
100
+ --help, -h Show this help
101
+
102
+ `);
103
+ p.line(`${C.bold}Tools an MCP client gets:${C.reset}`);
104
+ tools(p, names, true);
105
+ p.line();
106
+ p.line(`${C.bold}What an MCP client cannot do:${C.reset}`);
107
+ for (const c of CANNOT) p.item(c);
108
+ p.warn(SHELL_RISK);
109
+ p.hint('The module tools read ~/.jos/modules.json, ~/.jos/modules and ~/.jos/runs only.');
110
+ p.hint('Details: ' + SECURITY);
111
+ p.line();
112
+ connect(p, root, env);
113
+ p.line();
114
+ p.hint('The HTTP file explorer is disabled until F3.');
115
+ p.line();
116
+ }
117
+
118
+ // The notice on stderr when stdin is a terminal. p: a printer on stderr; names: the tools this server offers.
119
+ function notice(p, context, names) {
120
+ const { C } = p;
121
+ p.header('JOS SERVE', 'MCP server on stdio, waiting for a client');
122
+ p.warn('This is an MCP server: it reads JSON-RPC 2.0 on stdin and answers on stdout.');
123
+ p.hint('An MCP client starts this command and talks to it; typing here is not needed.');
124
+ p.line();
125
+ p.field('Root', context.root);
126
+ const on = [context.allowWrite && 'write_file (--allow-write)', context.allowShell && 'run_command (--allow-shell)'].filter(Boolean);
127
+ p.field('Mode', on.length ? `read-only plus ${C.yellow}${on.join(', ')}${C.reset}` : 'read-only');
128
+ p.field('Denied', '~/.jos/secrets, and any .master or vault.json under ~/.jos');
129
+ if (context.allowShell) p.warn(SHELL_RISK);
130
+ p.line();
131
+ p.line(`${C.bold}Tools:${C.reset}`);
132
+ tools(p, names, false);
133
+ p.line();
134
+ connect(p, context.root, context.env);
135
+ p.line();
136
+ }
137
+
138
+ module.exports = { help, notice, USES, BIN, onPath, SHELL_RISK };