@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.
- package/LICENSE +208 -21
- package/PROMISES.md +52 -0
- package/README.md +334 -138
- package/SECURITY-MCP.md +117 -0
- package/SECURITY-SECRETS.md +108 -0
- package/STYLE.md +23 -0
- package/bin/jos +170 -160
- 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 +57 -40
- 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 +128 -162
- 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 +194 -244
- package/src/commands/runs.js +102 -0
- package/src/commands/secrets.js +717 -137
- package/src/commands/start.js +210 -0
- package/src/commands/validate.js +178 -194
- package/src/commands/verify.js +56 -98
- 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 +48 -781
- package/src/ui.js +98 -0
- package/src/adapters/index.js +0 -16
- package/src/adapters/native.js +0 -41
- package/src/adapters/ollama.js +0 -140
- package/src/commands/add.js +0 -145
- package/src/commands/get.js +0 -245
- package/src/commands/llm.js +0 -159
- package/src/commands/prompts.js +0 -229
- package/src/commands/repo.js +0 -139
- package/src/utils/crypto.js +0 -41
- package/src/utils/request.js +0 -41
package/README.md
CHANGED
|
@@ -1,138 +1,334 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
cd jos-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
jos
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
| `jos
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
1
|
+
# @josfox/jos
|
|
2
|
+
|
|
3
|
+
A `.jos` is an intention you declare: **what** to do, **when it is done** (success criteria) and **what to avoid**
|
|
4
|
+
(guardrails). It is **validated without running anything**, and **run with guarantees**: limits enforced while it
|
|
5
|
+
runs, every criterion checked, a receipt per step and a hash-chained ledger per run. It has a **content identity**
|
|
6
|
+
(`art_`, the hash of the document; JOSID names such as `ent_`). It is **fractal**: an atom, a flow of atoms and a
|
|
7
|
+
folder with a `.jos/` directory have the same shape. And it is **reusable**: `use` another document by id,
|
|
8
|
+
instantiate templates, pin what you depend on by digest. Two reference kernels, JavaScript and Python, give
|
|
9
|
+
identical results on every conformance vector.
|
|
10
|
+
|
|
11
|
+
`jos` is the command line for this. It hands every document to the reference kernel `jos-core` and shows what the
|
|
12
|
+
kernel decided; it has no execution logic of its own.
|
|
13
|
+
|
|
14
|
+
Version 5.0.0 · kernel `@josfox/jos-core` 2.0.0 (its only dependency) · Node.js 18 or later ·
|
|
15
|
+
`jos` and `jos-core` open no network connection (a step you write can; see [Security](#security)) ·
|
|
16
|
+
Apache-2.0.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npm install -g @josfox/jos
|
|
22
|
+
jos --version # jos 5.0.0 and the jos-core it found
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`@josfox/jos` needs `@josfox/jos-core` from the registry: both packages have to be published for this to install.
|
|
26
|
+
`jos` finds the kernel as the installed `@josfox/jos-core`, else as `kernel/js/jos-core.js` of a checkout of the
|
|
27
|
+
repository; without one, `run`, `validate` and the other kernel commands exit 5 and say so.
|
|
28
|
+
|
|
29
|
+
## Start
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
jos # what jos does, and the .jos documents in this folder (read only)
|
|
33
|
+
jos start # writes ./jos-start: four examples that need no keys and no network
|
|
34
|
+
cd jos-start && jos run hello.jos
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`jos` alone lists the `.jos` / `.jos.json` documents of the current folder and one folder down, and the folders
|
|
38
|
+
with a `.jos/manifest.jos`, each with its `jos validate` and `jos run` command. It runs nothing, writes nothing, and
|
|
39
|
+
never scans your home folder. `jos start [dir]` writes `hello.jos` (an atom), `flow.jos` (a flow with a human gate),
|
|
40
|
+
`sealed/` (a folder sealed by its hash) and `limits.jos` (guardrails stopping two steps), plus a README with the
|
|
41
|
+
commands in order and what each prints; it never overwrites a file.
|
|
42
|
+
|
|
43
|
+
`jos run` shows the intention first, then the verdict of each criterion, then the outcome:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
$ jos run hello.jos
|
|
47
|
+
|
|
48
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
49
|
+
JOS RUN // jos-core 2.0.0
|
|
50
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
|
|
52
|
+
File: hello.jos
|
|
53
|
+
|
|
54
|
+
Intention: Print a greeting and exit cleanly
|
|
55
|
+
Done when:
|
|
56
|
+
• exit 0
|
|
57
|
+
• stdout contains "hello"
|
|
58
|
+
Avoid: "rm -rf", "curl", "wget"
|
|
59
|
+
Limit: 10 s, 65536 output bytes per stream, 0 retries
|
|
60
|
+
|
|
61
|
+
ID: art_803576e37d948d3036d1c84febcfc229f6fabc78ffebdbd1a427e9ca7d342575
|
|
62
|
+
Input version: 2.0.0
|
|
63
|
+
Capability: cap_… (local-trusted)
|
|
64
|
+
Ledger: ~/.jos/runs/run_…/ledger.jsonl
|
|
65
|
+
|
|
66
|
+
hello, .jos
|
|
67
|
+
|
|
68
|
+
Verdict:
|
|
69
|
+
[1] hello ✓ code 0
|
|
70
|
+
✓ exit: exit 0 (want 0)
|
|
71
|
+
✓ stdout_contains: found
|
|
72
|
+
|
|
73
|
+
✓ Intention met (code 0, run_…)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
When it is not met, the last line says why: `✖ Intention NOT met (code 1: not done)`, with the reason below it.
|
|
77
|
+
`--json` prints the kernel's result object instead, unchanged.
|
|
78
|
+
|
|
79
|
+
## What it can do
|
|
80
|
+
|
|
81
|
+
Each capability, the command, and the example of the tour (`examples/`, see [its README](examples/README.md)) that
|
|
82
|
+
shows it.
|
|
83
|
+
|
|
84
|
+
| Capability | Command | Tour |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| Declare an intention: objective, success criteria, `avoid`, `limit` | `jos init <name>`, `jos start`, `jos validate <file> --strict` | `01-hello.jos` |
|
|
87
|
+
| Check it without running anything: JSON, schema, normalization, `art_` | `jos validate <file>` | any |
|
|
88
|
+
| Every criterion evaluated and reported (`exit`, `stdout_contains`, `stdout_matches`, `stderr_empty`, files, hashes, `all` / `any` / `not`; text criteria are recorded, not evaluated) | `jos run <file>` | `02-criteria.jos` |
|
|
89
|
+
| Guardrails before and during the run: an `avoid` match never starts (3), time (124) and output (137) limits stop the process | `jos run limits.jos` (from `jos start`) | `limits.jos` |
|
|
90
|
+
| Flows: sequences and DAGs (`needs`), automatic and human gates, data between steps | `jos run <flow> --approve <step>`, `--flow <name>`, `--input <json>` | `03-flow-gate.jos` |
|
|
91
|
+
| Reuse: `use` by id and version range, templates with typed slots, a dependency pinned by digest | `jos run <flow> --library <dir>`, `jos run <template> --with <json>` | `04-use-template/` |
|
|
92
|
+
| A folder is a document; sealed by its directory hash, refused when anything in `.jos/` changes (3) | `jos run <folder>` | `05-sealed/` |
|
|
93
|
+
| Refuse what it cannot honor, before the first step (5): unmet `requires[]`, `security.tier: sandbox` | `jos run <file>` | `06-requires.jos`, `07-sandbox.jos` |
|
|
94
|
+
| Identity: `art_` of the content, `ent_` of the name, the hash of a folder | `jos ids <file>…` | `08-ids-ledger-meter.md` |
|
|
95
|
+
| Receipts and a hash-chained ledger of every run; verify it; meter it (tokens and cost when an engine reports them) | `jos runs`, `jos ledger verify <run>`, `jos meter <run>` | `08-ids-ledger-meter.md` |
|
|
96
|
+
| Credentials for a step without putting them in the document, the command line or the output | `jos secrets exec --only NAME -- jos run <file>` | `11-openai.jos`, `12-gemini.jos`, `13-anthropic.jos` |
|
|
97
|
+
| A local model as a shell step | `jos run 10-ollama.jos` | `10-ollama.jos` |
|
|
98
|
+
| A read-only page of your runs, on 127.0.0.1 only | `jos run 14-serve.jos` | `14-serve.jos` |
|
|
99
|
+
| Give an MCP client (Claude Code, Claude Desktop, Cursor) read-only access to a project folder | `jos serve --mcp` | see below |
|
|
100
|
+
| Reseal a 4.1.0 document (0.0.7 with `security.integrity_ref`) as 2.0, only if it verifies | `jos migrate <file>` | see Changes since 4.1.0 |
|
|
101
|
+
|
|
102
|
+
## Commands
|
|
103
|
+
|
|
104
|
+
| Command | What it does |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `jos` | Overview and read-only discovery of this folder (see Start). Exit 0. |
|
|
107
|
+
| `jos start [dir]` | Writes the four start examples and their README into `dir` (default `jos-start`), each validated by jos-core first; refuses (1) if any of them exists. |
|
|
108
|
+
| `jos run <file \| folder> [--json] [--dry-run] [--flow <name>] [--library <dir>] [--approve <step>]… [--input <json>] [--with <json>]` | Runs the document with jos-core, in the document's own folder (never the shell's current directory). A folder `D` (or `D/.jos`) with `D/.jos/manifest.jos` runs that manifest in `D` when it is of kind `directory` (a 1.0.0 flow with `meta.labels.dotjos` too); any other manifest is 2 before any step, as `jos validate D` says; a sealed folder is verified by its hash first. The options are passed to jos-core as they are: `--flow` picks a flow (default `main`, or the only one), `--library` is one more place for `use`, `--approve` approves a human gate (repeatable; `outer/step` for a nested one, `step#2` for a second occurrence), `--input` is the run's input, `--with` the slots of a template run directly. `--dry-run` checks everything and prints the plan without launching. `--` ends the options. |
|
|
109
|
+
| `jos validate <file> [--json] [--strict]` | Validates and normalizes; prints the `art_`, the input version, the intention and `Inferred:`, what jos-core applies that the document does not write (each JSON Pointer with its value and source: a SPEC section or a jos-core default), with `⚠ … not declared` for a missing objective, success criteria or guardrails. `--json` carries the same `inferred` section as `jos-core validate --json`. `--strict` also exits 2 when the objective, the success criteria or the guardrails are not written in the document. A folder `D` (or `D/.jos`) is validated as a directory by jos-core: a manifest that is not of kind `directory` is 2, the answer of `jos run D`; `--strict` applies to the manifest. Never runs anything. |
|
|
110
|
+
| `jos migrate <file> [--out <file> \| --in-place] [--force]` | For a 0.0.7 document sealed by `security.integrity_ref` (what `jos init` of 4.1.0 wrote): verifies the manifest exactly as `run` does (a mismatch is 3 and nothing is written), then writes `<name>.v2.jos` (or `--out`): jos-core's 2.0 normalization with `integrity_ref` removed, `security.permissions` moved to `security.x-legacy-permissions` (4.1.0 never applied it; 2.0 would apply an empty list as "no shell") and `integrity.hash` of the result. The result must validate and plan the same steps as the original before it is written; it prints what changed. Never overwrites the input without `--in-place`, nor an existing output without `--force`. A 2.0 document: "already 2.0" (0); a 1.1.0 or 1.0.0 document, or a 0.0.7 one without `integrity_ref`: 64 (they run as they are). |
|
|
111
|
+
| `jos ids <file \| folder>… [--json]` | `art_`, `ent_` (`uuid5("<kind>:<id>")`) and, for a folder, its directory hash and whether it matches its seal. Exit 2 if one is invalid. |
|
|
112
|
+
| `jos runs [--limit <n>] [--json]` | The runs in `~/.jos/runs/`, newest first: id, start, code, document, `art_`, metered cost. Each ledger is verified; one that does not verify is flagged. Never creates `~/.jos`. |
|
|
113
|
+
| `jos ledger verify <run_id \| ledger.jsonl \| run folder> [--json]` | Recomputes every entry hash of a run's ledger and checks the chain and the end entry. Exit 0 verifies, 1 does not, 2 not found. |
|
|
114
|
+
| `jos meter <run_id \| ledger.jsonl \| run folder> [--json]` | Steps, engines, attempts, time, tokens, cost, approvals, from the ledger, only when it verifies; a meter no engine reported is "not reported", never 0. |
|
|
115
|
+
| `jos init <name> [--force]` | Writes `<name>.jos`, a 2.0 atom validated by jos-core. A name ending in `.jos` / `.jos.json` gets no second extension; a directory part is kept (it must exist). |
|
|
116
|
+
| `jos verify` | Self-check: runs the embedded `examples/hello.jos` in a new temporary folder (also its HOME) and removes it. |
|
|
117
|
+
| `jos version`, `jos --version` | Host version; kernel version and where it was found. |
|
|
118
|
+
| `jos secrets set\|get\|list\|delete\|exec\|migrate\|fix-perms` | Local credentials in `~/.jos/secrets/`. Values never go on a command line or to stdout: `set NAME --stdin`, `get NAME` shows a keyed fingerprint, `exec --only A,B -- <cmd>` adds only those values to the child's environment. Details: [SECURITY-SECRETS.md](SECURITY-SECRETS.md). |
|
|
119
|
+
| `jos serve --mcp [--root <dir>] [--allow-write] [--allow-shell]` | MCP server over stdio, read-only by default. Details below and in [SECURITY-MCP.md](SECURITY-MCP.md). |
|
|
120
|
+
|
|
121
|
+
Every command has `--help`. `jos <file>` with a `.jos` file or a `.jos/` folder is not a command: it exits 1 and
|
|
122
|
+
names `jos run <file>` and `jos validate <file>`; nothing runs by itself. Every message for a person has one format
|
|
123
|
+
(`✓` / `✖` / `⚠`, color only on a terminal and without `NO_COLOR`, errors on stderr; rules and their test:
|
|
124
|
+
[STYLE.md](STYLE.md)); `--json` output is plain JSON.
|
|
125
|
+
|
|
126
|
+
### Exit codes
|
|
127
|
+
|
|
128
|
+
| Code | Meaning |
|
|
129
|
+
|---|---|
|
|
130
|
+
| 0 | done |
|
|
131
|
+
| 1 | not done (a criterion failed, a human gate is waiting) |
|
|
132
|
+
| 2 | invalid document |
|
|
133
|
+
| 3 | blocked (`avoid` matched, the Capability denied it, or an integrity hash or a 0.0.7 `integrity_ref` manifest does not match) |
|
|
134
|
+
| 4 | budget exceeded before start |
|
|
135
|
+
| 5 | unexecutable (no kernel, unresolved `use`, unmet `requires[]`, no engine for it, `sandbox` or `privileged` that this kernel cannot honor) |
|
|
136
|
+
| 124 | time limit |
|
|
137
|
+
| 137 | output limit or killed |
|
|
138
|
+
| 64 | usage error |
|
|
139
|
+
| 70 | internal error of the host (never a document outcome); the stack trace only with `JOS_DEBUG=1` |
|
|
140
|
+
|
|
141
|
+
### Files written
|
|
142
|
+
|
|
143
|
+
`jos` writes only what a command is for: `init` its file, `start` its folder, `migrate` its output file, `secrets` `~/.jos/secrets/`,
|
|
144
|
+
`serve --mcp --allow-write` what the client asks inside its root. jos-core writes a ledger for every `run` (not
|
|
145
|
+
`--dry-run`): `<run_id>/ledger.jsonl` in `.jos/replays/` of the nearest folder above the document that has a
|
|
146
|
+
`.jos/manifest.jos`, otherwise in `~/.jos/runs/`. The ledger holds the Capability, the receipts and hashes; a step's
|
|
147
|
+
output appears in it only as a hash. `jos`, `validate`, `ids`, `runs`, `ledger`, `meter`, `verify`, `version` and
|
|
148
|
+
`run --dry-run` leave nothing on disk.
|
|
149
|
+
|
|
150
|
+
## Security
|
|
151
|
+
|
|
152
|
+
What `jos` and jos-core guarantee, exactly:
|
|
153
|
+
|
|
154
|
+
- **Validating never executes.** `validate`, `ids`, `jos` alone and `run --dry-run` read and check; no step starts.
|
|
155
|
+
- **`avoid` is checked before launch.** A pattern that matches the engine input (the argv or command line) blocks
|
|
156
|
+
the step before its process exists: code 3. **`limit` is enforced while it runs**: past `seconds` the process
|
|
157
|
+
group is killed (124); past `output_bytes` on a stream it is killed (137); `retries` bounds the correction loop.
|
|
158
|
+
- **Hashes before running.** `integrity.hash` of a document, of a document reached by `use`, of a pinned
|
|
159
|
+
`requires[].digest` and of a sealed folder is recomputed (SHA-256 over the canonical JSON, RFC 8785) before
|
|
160
|
+
anything starts; a mismatch is code 3 (5 for an unmet digest). Signatures are not verified yet:
|
|
161
|
+
`security.tier: privileged` is refused (5).
|
|
162
|
+
- **A 0.0.7 document sealed by `security.integrity_ref`** (4.1.0's separate manifest) is verified against the octets
|
|
163
|
+
`jos run` read, before anything starts, `--dry-run` included: the manifest path must be relative and stay inside the
|
|
164
|
+
document's folder (no absolute path, URL or symbolic link), and its `artifact_sha256` must match the file (as it
|
|
165
|
+
is, or with LF / CRLF line endings); otherwise code 3 with a reason that begins `integrity_ref:`. A `type: jos` child
|
|
166
|
+
sealed this way is verified before the parent's first step. Like `integrity.hash`, it detects change; it does not
|
|
167
|
+
authenticate anyone (whoever can edit the folder can rewrite both files). `jos validate` does not read the manifest.
|
|
168
|
+
- **It refuses what it cannot honor**, before the first step: an unmet `requires[]` entry, `security.tier: sandbox`
|
|
169
|
+
(this kernel cannot confine a process), an engine it does not have, a criterion it cannot evaluate: code 5.
|
|
170
|
+
- **A step sees only the environment it declares**: the names in `execution.env` (default `PATH`).
|
|
171
|
+
- **Every run is recorded**: a receipt per step and a hash-chained ledger that `jos ledger verify` recomputes.
|
|
172
|
+
- **`jos secrets`**: AES-256-GCM per entry with the name as associated data, a master key at mode 0600 in a 0700
|
|
173
|
+
folder, values never on a command line or stdout, `exec --only` for the one run that needs them.
|
|
174
|
+
- **`jos serve --mcp` is read-only by default**; `~/.jos/secrets` is denied to its file tools; writing and running
|
|
175
|
+
programs are separate opt-ins (`--allow-shell` is local code execution as you).
|
|
176
|
+
- **No network from `jos` or jos-core**, no telemetry. A step is a program you chose: it can use the network (the
|
|
177
|
+
provider examples call `curl`; `14-serve.jos` listens on 127.0.0.1) and it runs with your user's privileges; the
|
|
178
|
+
`shell` engine is not a sandbox, and its receipts say `isolation: "none"`.
|
|
179
|
+
|
|
180
|
+
`jos secrets` is a local, best-effort vault for a single user, not a hardened secrets manager. Provided "AS IS"
|
|
181
|
+
without warranty (Apache-2.0 §7–8); use at your own risk.
|
|
182
|
+
|
|
183
|
+
### Disclaimer
|
|
184
|
+
|
|
185
|
+
Figures jos prints (cost, tokens, time) are estimates; vendor invoices are authoritative. Any AI output it relays can
|
|
186
|
+
be wrong: use the receipts and the ledger as evidence for your own decision. Provided "AS IS", without warranty
|
|
187
|
+
(Apache-2.0). The same text is the last line of `jos --help`.
|
|
188
|
+
|
|
189
|
+
## Not yet
|
|
190
|
+
|
|
191
|
+
| What | Today | Planned |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `engine: llm` and `judge` / `json_schema` criteria | valid documents, refused at run: code 5 | 2.1 |
|
|
194
|
+
| Signatures (`integrity.signature`, `security.tier: privileged`) | code 5 | 2.1 |
|
|
195
|
+
| Cache, crystals (`kind: crystal`), store | crystals validate; running one is code 5 | 2.1 |
|
|
196
|
+
| `jos get` (fetch a document from a registry) | frozen: exit 1 | 2.1, with signatures |
|
|
197
|
+
| Learning (`adaptive_ai` of 0.0.7 is kept as `x-legacy-adaptive_ai` and not read) | none | 2.1 |
|
|
198
|
+
| `jos serve` as an HTTP explorer | off: exit 2 (`jos serve --mcp` works) | F3 |
|
|
199
|
+
| `jos llm`, `jos prompts`, `jos repo`, `jos add` | frozen: exit 1, `jos <name>` says why | not scheduled |
|
|
200
|
+
| `josctl`, `@josfox/jos-cli` | separate older packages, not part of 5.0 | future |
|
|
201
|
+
|
|
202
|
+
## Compatibility
|
|
203
|
+
|
|
204
|
+
| Input | What happens |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `.jos` 2.0.0 | the format of this release |
|
|
207
|
+
| 1.1.0 short form (`{ "use": … }`) | normalized to a 2.0 atom with a `main` flow |
|
|
208
|
+
| 1.0.0 | normalized in memory to 2.0 (the file is not rewritten). Since jos-core 2.0.0-rc.8: `execution.flow` (`[{ invoke }]`) runs as the `main` flow, `execution.budget` bounds flows too, a gate keyed `after-<step>` is the gate of that step, and a 1.0.x document without `integrity.signature` may carry the 1.0.0 seal (hash computed with only the hash field empty) |
|
|
209
|
+
| 0.0.7 with text steps | normalized to a 2.0 flow and runs; `success_criteria` text is recorded, not evaluated |
|
|
210
|
+
| 0.0.7 sealed by `security.integrity_ref` | runs only if its manifest verifies (3 otherwise, also with `--dry-run`); `jos migrate` reseals it as a 2.0 document with `integrity.hash` |
|
|
211
|
+
| 0.0.7 written by `jos init` of 4.1.0 (with `orchestration.state`) | valid and runs since jos-core 2.0.0-rc.8: `orchestration.state` is kept as `orchestration.x-legacy-state` (never dropped), the `integrity_ref` manifest is verified first, `jos migrate` writes the 2.0 document (`read-0.0.7-init` in [PROMISES.md](PROMISES.md)) |
|
|
212
|
+
| 0.0.7 with object steps | invalid (2) |
|
|
213
|
+
| 0.0.7 with `type: prompt` steps (or any `type` other than `shell`, `jos`, `http`) | invalid (2) since jos-core 2.0.0-rc.8 (rc.3 to rc.7: valid and unexecutable, 5) |
|
|
214
|
+
| A step definition with a member the schema does not give it (`on_error` or `env` beside `run`, a `with` that is not an object) | invalid (2) since jos-core 2.0.0-rc.8: write `on_error` and `env` in the definition's `execution` |
|
|
215
|
+
| Integer values outside ±2^53 and below 10^21 however written (`9007199254740993`, `1e20`), decimals an IEEE 754 double does not keep exactly (`1.00000000000000001`, `1e-400`) | invalid (2) since jos-core 2.0.0-rc.8 (they were rounded, so two documents could share an `art_`, and `1e20` has no valid canonical form) |
|
|
216
|
+
|
|
217
|
+
`jos` reads 4.1.0's 13-key Minimal Valid Artifact unchanged: `jos validate` passes and `jos run` prints `Hello!`.
|
|
218
|
+
|
|
219
|
+
Every promise in one table, with what 4.1.0 did, what 5.0 does, what is planned and the command that proves it, known
|
|
220
|
+
gaps included: [PROMISES.md](PROMISES.md) (generated from `promises.json`; `npm test` runs every gate).
|
|
221
|
+
|
|
222
|
+
## Changes since 4.1.0
|
|
223
|
+
|
|
224
|
+
- `run` and `validate` hand the document to jos-core. 4.1.0's own runner is gone: `execSync` of each step, the
|
|
225
|
+
13-key presence check (only `jos` and `meta` are required now), `--insecure` and `--verbose` (both exit 64).
|
|
226
|
+
- `--flow <name>` exists again, with jos-core's meaning: it picks a flow of the document. A document with several
|
|
227
|
+
flows and none named `main` needs it.
|
|
228
|
+
- 0.0.7 flows with text steps run; object steps are invalid (2); `type: prompt` steps are invalid (2).
|
|
229
|
+
- Documents are validated with the published `schema/jos.schema.json` (jos-core 2.0.0-rc.8): integer values outside ±2^53
|
|
230
|
+
and below 10^21, and numbers not exactly representable, are invalid (2), as are members a definition does not have.
|
|
231
|
+
- `validate` shows what jos-core infers and what the document does not declare; `--strict` refuses (2) a document
|
|
232
|
+
without its own objective, success criteria and guardrails.
|
|
233
|
+
- `security.integrity_ref` is verified again (as in 4.1.0, stricter on paths), and `jos migrate` reseals it as
|
|
234
|
+
`integrity.hash`. A mismatch is code 3 (4.1.0: 1), `--dry-run` included; there is no bypass (`--insecure` is 64).
|
|
235
|
+
- Exit codes 0–5, 64, 70, 124, 137 (4.1.0 used 0 and 1).
|
|
236
|
+
- No more `~/.jos/runs/*/events.jsonl`: receipts in the result and jos-core's ledger replace it. `~/.jos/commands/`
|
|
237
|
+
overrides are not loaded; the command table is fixed.
|
|
238
|
+
- `init` writes a 2.0 atom (no `.sig.json`); `add`, `llm`, `prompts`, `get` and `repo` are frozen; `jos serve`
|
|
239
|
+
without `--mcp` is off.
|
|
240
|
+
- License Apache-2.0 (4.x: MIT). The `josfox` alias binary is no longer installed.
|
|
241
|
+
|
|
242
|
+
Full list: [CHANGELOG.md](CHANGELOG.md).
|
|
243
|
+
|
|
244
|
+
## Use with an MCP client
|
|
245
|
+
|
|
246
|
+
`jos serve --mcp` is an MCP server over stdio: the client starts the command and talks JSON-RPC 2.0 on its stdin and
|
|
247
|
+
stdout. Started by hand in a terminal, it prints on stderr what it is, the root, the mode, its tools and how to
|
|
248
|
+
connect it, then waits (Ctrl-C to exit). `jos serve --help` prints the connection commands with the real paths
|
|
249
|
+
filled in; run it in the project folder you want to serve:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
claude mcp add jos -- node /absolute/path/to/@josfox/jos/bin/jos serve --mcp --root /path/to/project
|
|
253
|
+
claude mcp remove jos
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
For Claude Desktop (`claude_desktop_config.json`) or Cursor (`.cursor/mcp.json`):
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"mcpServers": {
|
|
261
|
+
"jos": {
|
|
262
|
+
"command": "node",
|
|
263
|
+
"args": ["/absolute/path/to/@josfox/jos/bin/jos", "serve", "--mcp", "--root", "/path/to/project"]
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
| Tool | What it is for |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `list_files` | list the files and folders of a directory under the root (recursive to depth 3) |
|
|
272
|
+
| `read_file` | read a regular file under the root (at most 10 MB) |
|
|
273
|
+
| `search_files` | find files under the root by a glob on the name (`*` and `?`; 100 matches, depth 5) |
|
|
274
|
+
| `read_markdown` | read a Markdown file under the root, with its line count |
|
|
275
|
+
| `list_modules` | list the modules in `~/.jos/modules` and whether `~/.jos/modules.json` enables them |
|
|
276
|
+
| `list_processes` | list the runs recorded in `~/.jos/runs/*/report.json` |
|
|
277
|
+
| `hot_reload` | read `~/.jos/modules.json` again and report the enabled and disabled modules |
|
|
278
|
+
| `write_file` | only with `--allow-write`: create or overwrite a file under the root, never under `~/.jos` |
|
|
279
|
+
| `run_command` | only with `--allow-shell`: run a program with an argv (no shell) in a folder under the root; each one shown on stderr |
|
|
280
|
+
|
|
281
|
+
The server refuses to serve `/`, `$HOME` or a folder containing it unless `--root` names it. A client cannot reach a
|
|
282
|
+
file outside the root, cannot read, list, search or write `~/.jos/secrets` (nor any `.master` or `vault.json` under
|
|
283
|
+
`~/.jos`), cannot write without `--allow-write` and cannot run a program without `--allow-shell`.
|
|
284
|
+
**`--allow-shell` grants local code execution with your user privileges**: a program it starts can read your
|
|
285
|
+
secrets (`~/.jos/secrets`, `~/.ssh`, cloud keys) outside the MCP tools. Do not give it to an untrusted agent
|
|
286
|
+
without an external sandbox (a container or a virtual machine).
|
|
287
|
+
|
|
288
|
+
## Frozen
|
|
289
|
+
|
|
290
|
+
Their code stays in `src/commands/_frozen/` and is neither published nor dispatched; each has a one-line
|
|
291
|
+
`*.README.md` with the reason, which is published (`jos <command>` prints its path).
|
|
292
|
+
|
|
293
|
+
| Frozen | Reason |
|
|
294
|
+
|---|---|
|
|
295
|
+
| `llm` | Overwrote the `.jos` with model output without validating it; sent the artifact with the encrypted vault file as the Bearer token. |
|
|
296
|
+
| `prompts` | `optimize` overwrote the `.jos` without validating it; the adapters fabricated quality scores. |
|
|
297
|
+
| `get` | Wrote to a path built from `meta.name` (path traversal); downloaded without verifying. |
|
|
298
|
+
| `repo` | Managed the repository list that only `get` used. |
|
|
299
|
+
| `serve` (HTTP file explorer) | Served files by working directory without a sandbox and exposed `~/.jos/secrets` when run from the home directory. Exit 2. |
|
|
300
|
+
| `add` | Edited 0.0.7 flows and regenerated their `.sig.json` manifest (which `jos run` verifies again; `jos migrate` replaces it with `integrity.hash`). Flows are written in the document now (`flow.jos` of `jos start`, `examples/03-flow-gate.jos`). |
|
|
301
|
+
|
|
302
|
+
## Tests
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
npm test # the gate, in order; no network (Node 22 or later):
|
|
306
|
+
# jos verify; every test/*.test.js once with a per-file coverage gate (each published
|
|
307
|
+
# file of src/ and bin/jos >= 90 % lines, branches, functions; secrets.js and mcp.js 100 %);
|
|
308
|
+
# secrets and mcp alone at 100 %; nothing new left in HOME or in the repository
|
|
309
|
+
npm run test:host # host tests alone, without coverage (Node 18 or later)
|
|
310
|
+
npm run test:promises # the promise gate alone: every gate of promises.json (Node 18 or later)
|
|
311
|
+
npm run promises:md # regenerate PROMISES.md after editing promises.json
|
|
312
|
+
python3 -I bench/bench.py # host overhead over jos-core, appends to BENCHMARKS.md
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`test/tour.test.js` runs the tour: every example validates in both kernels with the same `art_`; the ones without
|
|
316
|
+
keys run; the provider and Ollama examples run against a fake `curl` (and a fake `ollama`) on `PATH`, with and without
|
|
317
|
+
a key, and the key is checked absent from the output, the result, the receipts and the ledgers. The tests write only
|
|
318
|
+
to temporary folders; run them as `HOME=$(mktemp -d) npm test` on a machine where other `jos` processes write to
|
|
319
|
+
`~/.jos`.
|
|
320
|
+
|
|
321
|
+
`test/promises.test.js` runs every gate of `promises.json` in a temporary folder with a temporary HOME, no provider
|
|
322
|
+
key and a fake `curl` and `ollama` on `PATH`. A known gap asserts today's behaviour and prints `⚠ known gap: <id>`;
|
|
323
|
+
it fails the day the gap is fixed, and then the entry is updated. It also fails when a row of "What it can do" or
|
|
324
|
+
"Not yet" has no entry, or when `PROMISES.md` is stale.
|
|
325
|
+
|
|
326
|
+
## Links
|
|
327
|
+
|
|
328
|
+
Specification, schemas and conformance vectors: <https://github.com/josfox-jos/jos-spec>. Source:
|
|
329
|
+
<https://github.com/josfox-jos/jos>.
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
Apache License 2.0 ([LICENSE](LICENSE)), from version 5.0.0; versions up to 4.x were published under the MIT
|
|
334
|
+
license. Copyright JOSFOX LLC.
|
package/SECURITY-MCP.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# `jos serve --mcp` — what an MCP client can and cannot do
|
|
2
|
+
|
|
3
|
+
Applies to `@josfox/jos` 5.0.0-rc.3 and later (verified again under 5.0.0) (closes H-01). Code: `src/mcp.js`, flag parsing in `src/serve.js`. Tests: `test/mcp.test.js` (gate: `npm run test:mcp`, 100 % lines, branches and functions of `src/mcp.js`).
|
|
4
|
+
|
|
5
|
+
`jos serve --mcp` speaks JSON-RPC 2.0 over stdio to one MCP client, usually an AI agent. Treat that client as untrusted: it acts on whatever text it has read, and a hostile web page, issue or file can steer it. The server is what stands between that client and the files of the user who started it.
|
|
6
|
+
|
|
7
|
+
## The problem this fixes (H-01)
|
|
8
|
+
|
|
9
|
+
Up to 5.0.0-rc.2 the file tools accepted any path under the working directory **and under all of `~/.jos`**, compared lexically (no symlink resolution), `write_file` and `toggle_module` were always on, and `run_command` ran any command line through a shell (`execSync`) behind a list of five forbidden substrings. A client could read `~/.jos/secrets/.master` and `vault.json` directly, or through a symlink in the working directory, and decrypt every stored secret; or `cat` them through `run_command`.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
jos serve --mcp [--root <dir>] [--allow-write] [--allow-shell]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Flag | Default | Effect |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| (none) | read-only, root = working directory | `list_files`, `read_file`, `read_markdown`, `search_files`, `list_modules`, `hot_reload`, `list_processes` |
|
|
20
|
+
| `--root <dir>` | the working directory | the one directory the file tools may reach. Must exist (exit 64 otherwise, or when the value is missing) |
|
|
21
|
+
| `--allow-write` | off | adds `write_file` (inside the root, never under `~/.jos`) |
|
|
22
|
+
| `--allow-shell` | off | adds `run_command` (a program and an argv, no shell): **local code execution with your user privileges**, see [`--allow-shell`](#--allow-shell) |
|
|
23
|
+
|
|
24
|
+
**The server refuses to start (exit 2)** when the root would be `/`, your home directory, or any directory that contains your home directory, unless you named it yourself with `--root`. The home directory is the parent of the `~/.jos` the host uses.
|
|
25
|
+
|
|
26
|
+
A tool that is off is not in `tools/list`, and calling it is `-32601 Unknown tool`.
|
|
27
|
+
|
|
28
|
+
**Started by hand.** When stdin is a terminal (a person, not a client), the server writes a notice on **stderr**: that it is an MCP server waiting for a client, the root, the mode (read-only, or which of `--allow-write` / `--allow-shell` is on), that `~/.jos/secrets` and any `.master` or `vault.json` under `~/.jos` are denied, with `--allow-shell` the warning that the flag is local code execution with the user's privileges, the tools it offers with one line each, and how to connect it (`claude mcp add jos -- node <absolute path of this bin/jos> serve --mcp --root <root>`, `claude mcp remove jos`, and the JSON block for Claude Desktop / Cursor), then `Ctrl-C to exit.` Nothing of it goes to stdout, the JSON-RPC channel. When stdin is not a terminal, which is how a client starts the server, there is no notice. The examples it prints are read-only; `jos serve --help` prints the same, for the current directory or the `--root` given with `--help`, and warns when that root is `/`, `$HOME` or contains it.
|
|
29
|
+
|
|
30
|
+
## Roots
|
|
31
|
+
|
|
32
|
+
| Tool | Reaches | Notes |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `list_files`, `read_file`, `read_markdown`, `search_files` | the root, nothing else | `~/.jos` is **no longer a root**. If the root contains `~/.jos` (only possible with an explicit `--root`), the server says so on stderr at start; `~/.jos/secrets` stays denied |
|
|
35
|
+
| `write_file` (`--allow-write`) | the root, minus everything under `~/.jos` | |
|
|
36
|
+
| `list_modules`, `hot_reload` | read `~/.jos/modules.json`, `~/.jos/modules/*/package.json` | fixed names; the client chooses no path |
|
|
37
|
+
| `list_processes` | reads `~/.jos/runs/*/report.json` | fixed names; reports liveness with `kill(pid, 0)` |
|
|
38
|
+
| `toggle_module` | **removed** | it wrote `~/.jos/modules.json`; nothing writes under `~/.jos` any more |
|
|
39
|
+
| `run_command` (`--allow-shell`) | anything the user can do | see below |
|
|
40
|
+
|
|
41
|
+
## How a path is checked
|
|
42
|
+
|
|
43
|
+
1. The client's string must be a string without NUL; it is resolved against the root (`..`, `.`, repeated slashes collapse here).
|
|
44
|
+
2. The result is turned into its **real path** with `fs.realpathSync.native` (which also returns the on-disk case on APFS). For a path that does not exist yet, the real path of its deepest existing ancestor is taken and the missing components are appended. A dangling symlink, a symlink loop, a component that is not a directory, or an unreadable component on the way makes the path **denied**.
|
|
45
|
+
3. The real path must be the root or lie under it. A symlink inside the root that points outside it (for example to `~/.ssh`) is therefore denied.
|
|
46
|
+
4. Neither the lexical nor the real path may be protected by name (next section); for a write, neither may lie under `~/.jos`.
|
|
47
|
+
5. The **device and inode** of the path and of each of its existing ancestors are compared with those of `~/.jos/secrets` and of every entry in it (and, for a write, of `~/.jos`). This catches hard links, case or Unicode folding the name checks missed, and any other alias of the same directory.
|
|
48
|
+
6. The file is opened **by that real path** with `O_NOFOLLOW` (and `O_NONBLOCK`, so a FIFO in the tree cannot hang the server); after opening, its inode is compared again. Writes open without `O_TRUNC`, check the inode, and only then truncate through the same descriptor, so a hard link to a secret is never emptied.
|
|
49
|
+
|
|
50
|
+
Listing and searching apply the same rules to every entry they meet: protected entries, hard links to a secret, and symlinks that leave the root, dangle or end in a protected place are left out (not reported as errors).
|
|
51
|
+
|
|
52
|
+
## What is protected
|
|
53
|
+
|
|
54
|
+
- `~/.jos/secrets` and everything below it, in its lexical form, its real form and by inode (so a `~/.jos` that is itself a symlink, or a secrets directory reached by its real location inside the root, is still denied);
|
|
55
|
+
- any file named `.master` or `vault.json` anywhere under `~/.jos` (for example a stray copy);
|
|
56
|
+
- for writes, everything under `~/.jos`.
|
|
57
|
+
|
|
58
|
+
The comparison of names ignores case and Unicode normalization (APFS and NTFS are case-insensitive; on APFS even `ſ`, U+017F, is the same letter as `s`, which the real-path and inode steps catch). On a case-sensitive file system this also denies, for instance, `~/.jos/SECRETS`, which is a different directory there: over-denial, never under-denial.
|
|
59
|
+
|
|
60
|
+
The protected set is recomputed on every call, so a vault created while the server runs is protected from its first byte; a path under `~/.jos/secrets` is denied by name even before the directory exists, so a client cannot learn whether it does.
|
|
61
|
+
|
|
62
|
+
## Errors
|
|
63
|
+
|
|
64
|
+
Out of the root and protected give the same message, `Access denied: path is outside the working directory or protected`, whether the file exists or not. Other errors name only the path the client wrote and an error code (`File not found: x`, `Cannot open x: EACCES`, `Not a regular file: x`). No error and no response carries the content of a denied file or a key; the tests check every response and stderr for the synthetic master key, ciphertext and secret value.
|
|
65
|
+
|
|
66
|
+
## `--allow-shell`
|
|
67
|
+
|
|
68
|
+
**Warning: `--allow-shell` grants local code execution with the privileges of the user who runs `jos`.** A program started by `run_command` can read the secrets (`~/.jos/secrets`, `.master`, `~/.ssh`, cloud credentials) outside the MCP tools: the root, the path checks and the denials of this document bind the file tools, not that program. Do not give `--allow-shell` to an untrusted agent, or to a client that relays one, without an external sandbox (a container or a virtual machine that holds neither your home directory nor your keys). `jos serve --help` and the notice at start (stdin a terminal) print this warning, and the stderr warning at start of a server with the flag repeats it.
|
|
69
|
+
|
|
70
|
+
`jos serve --mcp --allow-shell` adds `run_command { command, args?, cwd?, timeout? }`:
|
|
71
|
+
|
|
72
|
+
- `command` is a program (looked up in `PATH`, or a path) and `args` an array of strings. It is started with `spawnSync(command, args, { shell: false })`: no command line, no expansion, no `;`, `|`, `$(...)` or globbing. `"echo hi; touch x"` as `command` is a program name that does not exist (`ENOENT`). A client that wants a shell has to ask for one explicitly (`command: "sh", args: ["-c", ...]`) — the flag allows that too; see below.
|
|
73
|
+
- `cwd` (default: the root) is checked like any path: inside the root, not protected; the root of the file system (`/`) is refused even when it is the root.
|
|
74
|
+
- At start the server writes a warning on stderr; **every command** is written to stderr before it runs (`▶ jos serve: run_command ["prog","arg"] in "<dir>"`, JSON-quoted so no terminal escape gets through).
|
|
75
|
+
- The child's environment is the server's minus every variable whose name (compared without case) is a secret name in `~/.jos/secrets/vault.json` or `~/.jos/secrets/keychain.json`. That also removes whatever `jos secrets exec --only A,B -- jos serve ...` put there. If either file exists but cannot be read or has another shape, the command is refused (better no command than one with a secret in its environment).
|
|
76
|
+
- stdin is `/dev/null` (never the protocol stream); timeout defaults to 30 s and is capped at 10 min; output is capped at 1 MiB.
|
|
77
|
+
- The old list of forbidden substrings (`rm -rf /`, `mkfs`, ...) is **gone**: it never was a defence (`rm -rf "/"` or `rm -fr /` passed it) and keeping it would suggest one.
|
|
78
|
+
|
|
79
|
+
**What `--allow-shell` does not protect: anything.** It runs any program as you. `cat ~/.jos/secrets/.master`, `sh -c ...`, reading `~/.ssh`, `~/.aws` or browser profiles, and `jos secrets exec` all work. No shell parsing only removes injection through a command line; stripping secret names from the environment only stops the secrets already in the server's environment from being handed over for free. Use the flag only with a client you would let type in your terminal.
|
|
80
|
+
|
|
81
|
+
## What this does not protect
|
|
82
|
+
|
|
83
|
+
- **The root itself.** The client reads every file in it, dot files included when named (`read_file('.env')`; listings and searches skip dot files). With `--allow-write` it can overwrite any of them, including files that run code later: `.git/hooks/*`, `package.json` scripts, `Makefile`. Serve a project directory.
|
|
84
|
+
- **Races.** Steps 2–6 are not atomic. `O_NOFOLLOW` covers the last component and the inode is compared after opening; a process of the same user that swaps an intermediate directory for a symlink between the check and the open can still redirect the access. Such a process can read the secrets directly anyway.
|
|
85
|
+
- **Other users of the same uid**, root, and anything with a copy of `.master` (see SECURITY-SECRETS.md).
|
|
86
|
+
- **Secrets that are not in the vault**: environment variables with other names (`AWS_SECRET_ACCESS_KEY` set by your shell), credential files inside the root.
|
|
87
|
+
- **Copies** (not hard links) of a secret file outside `~/.jos`, or under `~/.jos` with a name other than `.master` or `vault.json`, for example a backup someone left in the root. A hard link to the original is denied by inode wherever it is.
|
|
88
|
+
- **Windows**: the code has no platform branch and drops the flags Windows lacks, but it has not been run there.
|
|
89
|
+
|
|
90
|
+
## Tests and coverage
|
|
91
|
+
|
|
92
|
+
`test/mcp.test.js`, 40 tests, in-process (calling `handleMessage` and `startMCPServer` with streams) and over the real stdio protocol (`bin/jos serve --mcp`), each in its own temporary `HOME` with a synthetic vault (one of them written by the real `jos secrets set --stdin`). `JOS_SECRETS_BACKEND` and `JOS_TEST_KEYCHAIN` are removed from the test process and from every child.
|
|
93
|
+
|
|
94
|
+
- M1 every spelling of a path into the secrets (relative, absolute, `..`, `./`, case, `ſ` on macOS, repeated slashes, the directory itself, missing files under it, before the directory exists, a symlinked `~/.jos`, a root inside the secrets) is denied for all five file tools; out-of-root paths get the same message whether they exist or not;
|
|
95
|
+
- M2 a symlink and a hard link in the root to `.master` are denied and hidden; symlinks out of the root, dangling, looping or through a file are denied, a symlink inside the root works;
|
|
96
|
+
- M3 `run_command` is absent and unknown without `--allow-shell`; with it, it runs an argv without a shell (metacharacters stay bytes), is announced on stderr, and `env` in the child shows none of the vault and keychain names (also in other case); `cwd` outside the root or `/` is refused; refusal when the names cannot be read; failure, signal, timeout and bad `cwd` are reported;
|
|
97
|
+
- M4 reads, overwrites, listings, recursive listings and glob searches inside the root keep working; FIFO, `EACCES`, size limit and invalid parameters give clean errors;
|
|
98
|
+
- M6 read-only by default (`write_file` and the removed `toggle_module` are unknown tools); with `--allow-write` nothing is written under `~/.jos`, from a root that contains it or from a root inside it;
|
|
99
|
+
- M7 `/`, `$HOME` and an ancestor of it are refused without `--root` (exit 2), accepted with it; `--root` without a value or to a missing directory exits 64;
|
|
100
|
+
- the notice: with stdin a terminal it is on stderr and stdout stays empty, it lists only the tools that are on and carries an existing absolute path of `bin/jos` and the root in its examples; without a terminal there is none; `--help` lists every tool with its flag, has no `undefined`, and its examples carry real paths (and the short `jos` form only when `jos` on `PATH` is this `bin/jos`); `--help`, and the notice when `--allow-shell` is on (only then), carry the warning that the flag is local code execution and needs an external sandbox for an untrusted agent;
|
|
101
|
+
- module tools: `modules.json` as a symlink or a hard link into the secrets reads as missing; `~/.jos/runs` or `~/.jos/modules` as a link to `~/.jos/secrets` lists nothing and reads nothing from it, with a file planted one level inside the secrets that only the `homePath` check (real path, then name denial) stops, since the inode comparison after opening covers only the direct entries of the secrets directory (R-18);
|
|
102
|
+
- a symlink swapped in for the last component between the path check and the open (the test wraps `fs.openSync`, the call that opens the file) is refused for `read_file` and for `write_file`, with a decoy inside the root that is not a secret, so only `O_NOFOLLOW` can stop it (R-17).
|
|
103
|
+
|
|
104
|
+
Coverage of `src/mcp.js`: 100 % lines, branches and functions, without exclusions. Two tests are skipped when run as root (`EACCES` cannot be produced) and three on Windows (FIFO, and the two `O_NOFOLLOW` tests, which say so: the flag does not exist there); in those environments the gate cannot reach 100 % branches.
|
|
105
|
+
|
|
106
|
+
Sensitivity, checked by mutation on a scratch copy (each mutant must make `npm run test:mcp` fail): removing the path jail, the realpath step, the root check, the name denial, the ancestor inode check, the inode check at open, the `~/.jos` write block, the write flag, the `--allow-write` or `--allow-shell` gate, `shell: false`, the environment filter, `O_NONBLOCK` (the FIFO test then hangs and is killed), `O_NOFOLLOW` (the two swap tests then read or write through the symlink and fail), the realpath or the protection in `homePath` (the two `~/.jos/runs`/`~/.jos/modules` link tests then list the planted entry), the listing filter, the refusal to serve `$HOME` or the `cwd: "/"` check — every one fails the gate.
|
|
107
|
+
|
|
108
|
+
## Incompatible changes
|
|
109
|
+
|
|
110
|
+
- Read-only by default: `write_file` exists only with `--allow-write`, and never writes under `~/.jos`. `toggle_module` is removed.
|
|
111
|
+
- `run_command` exists only with `--allow-shell`, and takes a program plus an `args` array; a command line in `command` is no longer run through a shell.
|
|
112
|
+
- `jos serve --mcp` refuses to start in `/`, in `$HOME` or in a directory containing it, unless `--root` names the directory.
|
|
113
|
+
- The file tools no longer reach `~/.jos` from a root outside it.
|
|
114
|
+
- A symlink in the root that points outside it is no longer followed (denied, and hidden from listings).
|
|
115
|
+
- `search_files` takes a glob on the file name (`*`, `?`, anchored at both ends), no longer a regular expression made from the pattern; `hello` no longer matches `hello.txt`. The 100-match limit now holds within one directory too.
|
|
116
|
+
- `read_file` refuses unknown encodings and non-regular files; `write_file` refuses non-string content.
|
|
117
|
+
- Error texts for denied paths changed to the single message above.
|