mindsmith 0.1.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/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # Mindsmith
2
+
3
+ > **Pre-launch — nothing is field-adopted yet (rung 6/6 is empty on every bar below).**
4
+ > We publish maturity per feature and keep refuted claims on the page. Honesty is the product's USP,
5
+ > so it is also the documentation's. See [docs/honesty.html](docs/honesty.html).
6
+
7
+ **Mindsmith is an external reasoning layer for your local LLM — an auditable critical mind, gated
8
+ think-mode lanes, and verified-stock answers at zero frontier calls, from a mixture of certified
9
+ graph-experts fronting your model. OpenAI-compatible endpoint + MCP tools over shareable `.sgc` rooms;
10
+ nothing leaves your machine by default.**
11
+
12
+ A small quantized gguf fits your VRAM, but quantization costs it judgment — and on its own it has no
13
+ external check on its reasoning. Mindsmith, a thin surface over the
14
+ [skynet-graph](https://github.com/9pings/skynet-graph) engine, hands the model tools it doesn't have
15
+ alone: answers steered against a certified **method vocabulary** (covered queries served from verified
16
+ local stock at 0 frontier calls); gated **assistant lanes** (`propose` / `hint` — an external think-mode
17
+ that returns a tested verdict or refuses, never a guess); and an external **critical mind** (`critique` —
18
+ weigh a question over a witness-gated pool of arguments, with an honest, certification-aware verdict).
19
+ The judgment repair is measured on the same model at two quant levels (protocols in the engine repo):
20
+ SQL covered queries low-quant **8→63 %** (high-quant reference 46→92 %, N=201) · finance-table traffic
21
+ **7→62 %** (20→78 %, N=120) — zero big-model calls at runtime on the covered slice.
22
+
23
+ No accounts, no catalog, no phone-home. Stocks live in a local **room** you own: build them with the
24
+ engine's forge (0 false admissions across 3 datasets × 2 forge models; sha256 validation dossier per
25
+ stock), freeze them, share them as files, import someone else's — a malformed bundle is refused at the
26
+ gate, never written.
27
+
28
+ ## 60-second quickstart
29
+
30
+ ```bash
31
+ # Pre-launch note: not on npm yet. Until the release:
32
+ # git clone <this repo> && cd mindsmith && npm install && npm test # 26/26, GPU-free
33
+ # Once published, the same commands work via: npm i -g mindsmith or npx mindsmith
34
+
35
+ # 1 — serve (embedded gguf as the escalation):
36
+ FRONTIER_MODEL=/path/model.gguf mindsmith serve --room ./sgc
37
+ # → mindsmith → http://127.0.0.1:4747/v1 (OpenAI-compatible)
38
+
39
+ # 2 — point ANY OpenAI client at it (official SDKs, LangChain, Open WebUI, curl):
40
+ # baseURL = http://127.0.0.1:4747/v1 apiKey = anything
41
+ curl -si http://127.0.0.1:4747/v1/chat/completions \
42
+ -H 'content-type: application/json' \
43
+ -d '{"messages":[{"role":"user","content":"your question"}]}'
44
+ # every completion carries provenance headers:
45
+ # x-sg-served-from: local|frontier · x-sg-arm · x-sg-cost · x-sg-coverage · x-sg-saved · x-sg-sgc-version
46
+
47
+ # 3 — your stock rooms (local .sgc mini-repos, gate-checked):
48
+ mindsmith rooms list # inventory: name@version, classes, sha256, ❄ frozen
49
+ mindsmith rooms import ./fin-tables-stock.sgc # gate-checked (a bad bundle never lands)
50
+ mindsmith rooms freeze fin-tables-stock # writes the auditable sha256 dossier
51
+ mindsmith rooms export fin-tables-stock ./out # bundle + dossier, ready to share
52
+
53
+ # 4 — or as MCP tools for an agent host (Claude Code, any MCP client):
54
+ claude mcp add mindsmith -- mindsmith mcp --routing routing.json
55
+ # tools: ask · drift · metrics · lattice_load · hint · propose · critique
56
+ ```
57
+
58
+ `GET /healthz` is the ops readout (no key, no query content): policy, configured vs reachable tiers,
59
+ loaded `.sgc` versions, stock size.
60
+
61
+ ## Features × maturity
62
+
63
+ Maturity uses a fixed 6-rung scale — 1 coherent idea · 2 design with pre-registered kill-gates ·
64
+ 3 mechanics proven · 4 measured at scale · 5 product-integrated · 6 field-adopted. Nothing is at 6:
65
+ this is pre-launch. Scale details: [docs/honesty.html](docs/honesty.html).
66
+
67
+ | Feature | Maturity | Measured | Docs |
68
+ |---|---|---|---|
69
+ | **F1 — Low-quant repair** (certified stock steers the model; covered → 0 frontier calls) | `█████░` 5/6 product-integrated | SQL 8→63 % (N=201) · finance tables 7→62 % (N=120) · forge 0 false admissions | [docs](docs/features.html#f1) |
70
+ | **F4 — External think mode** (`propose` → gated verdict + blame + tested options; `hint` menu) | `█████░` 5/6 product-integrated | one dialogue round 17/24 → 24/24 at zero false admissions | [docs](docs/features.html#f4) |
71
+ | **F5 — External critical mind** (`critique`: witness gate, anchored generation, honest verdict) | `█████░` 5/6 surface · campaign numbers at 4/6 | coverage 77 % vs 58 % (48 args) · certified perimeter 12/24 → 24/24 · 0 fabrication in negative controls | [docs](docs/features.html#f5) |
72
+ | **F6 — Local `.sgc` rooms** (list/import/export/freeze, sha256 dossiers, engine-gated loads) | `█████░` 5/6 product-integrated | gate-checked import; loads never bypass the engine gates | [docs](docs/features.html#f6) |
73
+ | **F2 — Piece-by-piece zoom** (typed DAG on big tasks) | `████░░` 4/6 measured — **library-only today, not surfaced here (the known gap)** | math word problems ×3.25 [2.4–4.8] · financial-table QA ×2.54 [1.96–3.5], 560 tasks | [docs](docs/features.html#f2-gap) |
74
+
75
+ ## Why not just…?
76
+
77
+ - **…run a bigger model?** If it fits your VRAM, do. This exists for the model you *can* run:
78
+ certified-stock steering recovers most of what quantization broke (SQL 8→63 %, finance 7→62 %),
79
+ at 0 frontier calls on the covered slice.
80
+ - **…use the model's think mode / a self-critique prompt?** The 2024-25 literature (and our own
81
+ 3-form refutation) agree: self-critique underperforms *external* feedback with localized blame.
82
+ The `propose` gate and `critique` tool are that external feedback — structural, auditable, and
83
+ un-arguable-with (a forced write lands UNTRUSTED, never admitted).
84
+ - **…RAG / a prompt library?** Retrieval trusts whatever is indexed. A room only admits what
85
+ passes the gate (0 false admissions measured at the forge), and every completion tells you
86
+ which slice you can trust (`x-sg-served-from`, `x-sg-coverage`).
87
+ - **…an agent framework's memory?** None we checked reopens a task whose premise drifted; the
88
+ engine's typed task state retracts and reopens with the reason, at 0 model calls (surfaced here
89
+ via MCP `state_recall` / `plan_sync` on the engine side).
90
+
91
+ ## What actually runs
92
+
93
+ - **`serve`** — `POST /v1/chat/completions`, `GET /v1/models`, `GET /healthz`. Default port 4747,
94
+ binds 127.0.0.1 on purpose. A covered query is served from verified local stock at 0 frontier calls;
95
+ a miss escalates (you always get an answer); the local side never fabricates — 0 hallucination by
96
+ construction on the covered slice.
97
+ - **Escalation** — either a single frontier (`FRONTIER_MODEL=<path.gguf>` embedded, or `LLM_BASE=<url>`
98
+ any OpenAI-compatible endpoint), or **N-tier routing** (`--routing config.json`): ordered tiers, each
99
+ tagged with an egress class (`none` / `mid` / `frontier`), governed by a policy ceiling —
100
+ `no-egress` (default for routing configs) · `allow-mid` · `allow-all`. A query is **never** silently
101
+ sent to a forbidden tier: if the policy leaves nothing reachable, you get a typed `NO_REACHABLE_TIER`
102
+ refusal. The no-egress guarantee is enforced fail-closed on real sockets in the test suite, with a
103
+ negative control proving the guard has teeth.
104
+ - **`mcp`** — the same verified stock + escalation over stdio (no HTTP socket). Tools:
105
+ `ask` (local-first, `{answer, source, cached, cost}`) · `drift` (invalidate a stale entry) ·
106
+ `metrics` (economy readout) · `lattice_load` (learn THROUGH the gate — the only registry write path) ·
107
+ `hint` (SOFT lane: advisory certified-shape menu, no guarantee attached) · `propose` (HARD lane: the
108
+ gate never yields; `force=true` records untrusted provenance, never admits) · `critique` (below).
109
+ - **`rooms`** — `list | import <file> | export <name> <dest> | freeze <name>`. Import dry-loads the
110
+ bundle through the same gates the appliance uses; freeze writes the sha256 dossier that makes the
111
+ bundle a fixed, auditable reference.
112
+
113
+ ## The `critique` iteration contract
114
+
115
+ `critique` runs the external critical mind on a question: declared viewpoints established through a
116
+ witness gate over a statement pool, anchored generation of missing theses, a typed ledger, and a
117
+ certification-aware verdict. The contract: **OPEN ledger points and an UNDECIDED verdict are a typed
118
+ data request, not a dead end.** The tool cannot reach the web — the host (you, or your agent) gathers
119
+ real statements that bear on the OPEN points, then calls `critique` again with `statements: [...]`
120
+ (`"PRO: ..."` / `"CON: ..."` lines). The frame upgrades to MATERIAL and the margin can move honestly.
121
+ A verdict is mechanical only at the measured margin bound (≥3 on free/declared frames, ≥2 on a
122
+ certified perimeter); below it the deliverable is counts + coverage + an honest UNDECIDED — never a
123
+ fake weighing.
124
+
125
+ ## Honest limits (what is NOT claimed)
126
+
127
+ - **The guarantee is at stock admission, not at execution.** Runtime steering orients; a suggestion is
128
+ not a correctness proof. A runtime "trusted answers" cross-agreement tier was tested and **refuted**
129
+ — it was removed, and stays listed in [docs/honesty.html](docs/honesty.html).
130
+ - **The win lives on the typed, recurrent slice of your traffic.** Coverage depends on your stocks;
131
+ forge yield is per-domain; amortization is a property of the domain's stereotypy.
132
+ - **F2 zoom is not surfaced here.** The piece-by-piece decomposition is measured (rung 4/6) but
133
+ library-only in skynet-graph today; no MCP tool exposes it yet.
134
+ - **`critique` bounds**: below the measured decidability margin the verdict is UNDECIDED by design;
135
+ entry templates are not yet form-robustness-tested; on FREE frames coverage is relative to the pool
136
+ (the payload says so).
137
+ - Streaming is simulated; no per-tier timeout yet.
138
+ - **Nothing is field-adopted (rung 6/6)** — no external replications yet. This is pre-launch.
139
+
140
+ ## Env / flags
141
+
142
+ `FRONTIER_MODEL` (gguf, embedded) or `LLM_BASE` (any OpenAI-compatible endpoint) — the single-frontier
143
+ escalation · `--routing <config.json>` / `$SG_ROUTING` (N-tier) + `--policy` / `$SG_POLICY`
144
+ (`no-egress|allow-mid|allow-all`) · `LOCAL_MODEL` (gguf — semantic coverage: paraphrases hit the stock,
145
+ opt-in) · `--room <dir>` (default `./sgc`) · `--store <f.json>` (durable cross-restart stock,
146
+ default `.skynet-stock.json`) · `--port` (default 4747, binds 127.0.0.1).
147
+
148
+ ---
149
+
150
+ AGPL-3.0-or-later · © 2026 Nathanael Braun · solo-author project ·
151
+ engine: [github.com/9pings/skynet-graph](https://github.com/9pings/skynet-graph) ·
152
+ docs site: [`docs/`](docs/) (GitHub Pages) ·
153
+ **pre-launch — nothing is field-adopted yet (rung 6/6)**
package/bin/mindsmith ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // mindsmith — your low-quant local model, repaired and steered like a bigger one. Nothing leaves
4
+ // the machine. An OpenAI-compatible endpoint over the skynet-graph engine + your local room of verified
5
+ // `.sgc` stocks (community model: build/freeze/share/import your own — no catalog, no subscription).
6
+ //
7
+ // mindsmith serve [--port N] [--store <f.json>] [--room <dir>]
8
+ // escalation, either:
9
+ // N-tier routing: --routing <config.json> [--policy no-egress|allow-mid|allow-all]
10
+ // (tiers = backend presets + egress class; default no-egress)
11
+ // single frontier: env FRONTIER_MODEL=<path.gguf> (embedded) | LLM_BASE (endpoint)
12
+ // optional env LOCAL_MODEL=<path.gguf> (semantic coverage: paraphrases hit the stock)
13
+ // mindsmith mcp same flags/env as serve, NO http socket — the MCP tool surface (stdio) over the
14
+ // same verified stock + escalation; stdout = protocol.
15
+ // Register: claude mcp add mindsmith -- mindsmith mcp --routing <config.json>
16
+ // mindsmith rooms list | import <file.sgc> | export <name> <destDir> | freeze <name> [--room <dir>]
17
+ // your local SGC room: inventory (sha256 + frozen dossiers), gate-checked import
18
+ // (a malformed bundle is refused, never written), export with dossier, freeze
19
+ // (writes the auditable sha256 dossier). Forge new stocks with `sg forge` (skynet-graph).
20
+ const { createApp } = require('../lib/app.js');
21
+ const rooms = require('../lib/rooms.js');
22
+ const { resolveRouting } = require('../lib/serve-config.js');
23
+
24
+ const argv = process.argv.slice(2);
25
+ const cmd = argv[0];
26
+ const flag = ( n ) => { const i = argv.indexOf('--' + n); return i !== -1 ? argv[i + 1] : undefined; };
27
+ const roomDir = flag('room') || flag('sgc') || 'sgc';
28
+
29
+ // escalation resolution shared by serve + mcp: an N-tier routing config OR the legacy single frontier backend.
30
+ function resolveEscalation( need ) {
31
+ const routing = resolveRouting(argv, process.env);
32
+ if ( routing ) return { tiers: routing.tiers, policy: routing.policy };
33
+ let frontierChat;
34
+ if ( process.env.FRONTIER_MODEL ) frontierChat = require('skynet-graph/lib/providers/llm-local.js').makeLocalAsk({ modelPath: process.env.FRONTIER_MODEL, reasoningBudget: 0 });
35
+ else if ( process.env.LLM_BASE ) frontierChat = require('skynet-graph/lib/providers/llm.js').makeAsk({ base: process.env.LLM_BASE, model: process.env.LLM_MODEL || 'default' });
36
+ else { console.error(need + ' needs an escalation: --routing <config.json>, or env FRONTIER_MODEL=<path.gguf> | LLM_BASE=<url>'); process.exit(1); }
37
+ return { frontierChat };
38
+ }
39
+ function buildApp( escalation, extra ) {
40
+ const localAsk = process.env.LOCAL_MODEL ? require('skynet-graph/lib/providers/llm-local.js').makeLocalAsk({ modelPath: process.env.LOCAL_MODEL, reasoningBudget: 0 }) : undefined;
41
+ return createApp(Object.assign({}, escalation, {
42
+ localAsk,
43
+ store: flag('store') || '.skynet-stock.json', sgcDir: roomDir,
44
+ }, extra));
45
+ }
46
+
47
+ if ( cmd === 'rooms' ) {
48
+ const sub = argv[1];
49
+ try {
50
+ if ( sub === 'list' || sub === undefined ) {
51
+ const rows = rooms.list(roomDir);
52
+ if ( !rows.length ) { console.log('(empty room: ' + roomDir + ' — import a .sgc, or forge one: sg forge …)'); process.exit(0); }
53
+ for ( const r of rows ) console.log(r.invalid
54
+ ? '✗ ' + r.file + ' — INVALID: ' + r.invalid
55
+ : (r.frozen ? '❄ ' : '· ') + r.name + '@' + r.version + ' [' + r.kind + ', ' + r.classes + ' classes] sha256 '
56
+ + r.sha256.slice(0, 16) + '… (' + r.file + (r.frozen ? ' + ' + r.dossier : '') + ')');
57
+ } else if ( sub === 'import' ) {
58
+ const r = rooms.importBundle(roomDir, argv[2] || '');
59
+ console.log('imported ' + r.name + '@' + r.version + ' [' + r.kind + ', ' + r.classes + ' classes] → ' + roomDir + '/' + r.file + ' sha256 ' + r.sha256.slice(0, 16) + '…');
60
+ } else if ( sub === 'export' ) {
61
+ const r = rooms.exportBundle(roomDir, argv[2] || '', argv[3] || '.');
62
+ console.log('exported ' + r.exported.join(' + ') + ' → ' + (argv[3] || '.'));
63
+ } else if ( sub === 'freeze' ) {
64
+ const r = rooms.freeze(roomDir, argv[2] || '');
65
+ console.log('frozen: ' + r.dossier + ' sha256 ' + r.sha256.slice(0, 16) + '… (' + r.classes + ' classes)');
66
+ } else { console.error('usage: mindsmith rooms list|import <file>|export <name> <dest>|freeze <name> [--room <dir>]'); process.exit(1); }
67
+ } catch ( e ) { console.error(e.message); process.exit(1); }
68
+ } else if ( cmd === 'serve' || cmd === undefined ) {
69
+ const app = buildApp(resolveEscalation('serve'), {
70
+ port: Number(flag('port')) || 4747,
71
+ onAnswer: ( row ) => console.error('[' + (row.source === 'local' ? 'cache' : 'frontier') + '] ' + row.query),
72
+ onRoute: ( r ) => console.error('[tier:' + r.tier + '] ' + r.query)
73
+ });
74
+ app.start(( first ) => {
75
+ const l = first.loaded;
76
+ console.log('mindsmith → http://127.0.0.1:' + (Number(flag('port')) || 4747) + '/v1 (OpenAI-compatible)');
77
+ console.log('room ' + roomDir + ': methods=' + l.methods.length + ' lattice=' + l.lattice.length + (l.skipped.length ? ' skipped=' + l.skipped.length : ''));
78
+ }).catch(( e ) => { console.error(e.message); process.exit(1); });
79
+ process.on('SIGINT', () => { app.stop(); process.exit(0); });
80
+ } else if ( cmd === 'mcp' ) {
81
+ // the MCP stdio surface over the SAME verified stock + escalation as serve — no http socket.
82
+ // stdout is the PROTOCOL channel: everything human goes to stderr.
83
+ const app = buildApp(resolveEscalation('mcp'), { onAnswer: ( row ) => console.error('[' + (row.source === 'local' ? 'cache' : 'frontier') + '] ' + row.query) });
84
+ app.sync().then(( first ) => {
85
+ const { createMcpServer, defaultTools, startMcpStdio, stockWiring } = require('skynet-graph/lib/sg/mcp.js');
86
+ // the FULL tool surface over the room: proxy (ask/drift/metrics/lattice_load) + the ASSISTANT
87
+ // lanes over the room's methods stock (hint = certified menu · propose = gated admission) +
88
+ // the C9 critical mind on the escalation backend (critique).
89
+ const wiring = { proxy: app.proxy, critiqueAsk: app.escalationAsk };
90
+ const mf = first.loaded.methods[0];
91
+ if ( mf ) { try { Object.assign(wiring, stockWiring(JSON.parse(require('fs').readFileSync(require('path').join(app.sgcDir, mf), 'utf8')))); } catch ( e ) { console.error('room stock lanes unavailable: ' + e.message); } }
92
+ const server = createMcpServer({
93
+ tools: defaultTools(wiring),
94
+ serverInfo: { name: 'mindsmith', version: require('../package.json').version }
95
+ });
96
+ startMcpStdio({ server });
97
+ const l = first.loaded;
98
+ console.error('mindsmith mcp ready — tools: ' + server.tools.map(( t ) => t.name).join(', ')
99
+ + ' · room: methods=' + l.methods.length + ' lattice=' + l.lattice.length);
100
+ }).catch(( e ) => { console.error(e.message); process.exit(1); });
101
+ } else {
102
+ console.error('usage: mindsmith serve|mcp|rooms …');
103
+ process.exit(1);
104
+ }
package/lib/app.js ADDED
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+ /**
3
+ * mindsmith — the LOCAL APPLIANCE, a thin assembly over the skynet-graph engine:
4
+ *
5
+ * [OpenAI client] → baseURL → [this app: serve handler → C6 proxy → verified stock (.sgc, gated)]
6
+ * ↘ (miss) frontier chat (local gguf or endpoint)
7
+ * [local SGC ROOM] → loadBundles THROUGH the engine's gates (your own bundles: create/freeze/import/export)
8
+ *
9
+ * Guarantees carried from the engine: a covered query is served from VERIFIED stock at 0 frontier
10
+ * calls; the local side never fabricates (0 hallucination); a miss always answers. Personal data never
11
+ * leaves this process — the ONLY outbound destination is the declared frontier (no catalog, no phone-home).
12
+ *
13
+ * `createApp` takes injectable backends (tests run it with stubs, GPU-free); bin/mindsmith
14
+ * resolves the real ones (embedded gguf via env FRONTIER_MODEL, or an OpenAI-compat endpoint via LLM_BASE).
15
+ */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const Graph = require('skynet-graph');
19
+ const { createServeHandler, startServeServer } = require('skynet-graph/lib/sg/serve.js');
20
+
21
+ /** Load every pulled `.sgc` bundle into the proxy THROUGH the gates (kind-dispatched; never a raw write).
22
+ * `report.versions` = sorted `name@version` of what actually LOADED — the stock-freshness provenance. */
23
+ function loadBundles( px, dir ) {
24
+ const report = { methods: [], lattice: [], skipped: [], versions: [] };
25
+ if ( !fs.existsSync(dir) ) return report;
26
+ for ( const f of fs.readdirSync(dir) ) {
27
+ if ( !/\.(json|sgc)$/.test(f) ) continue;
28
+ let art;
29
+ try { art = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch ( e ) { report.skipped.push(f); continue; }
30
+ if ( !art || art.format !== 'sgc' ) { report.skipped.push(f); continue; }
31
+ try {
32
+ if ( art.kind === 'methods' ) { px.load(art); report.methods.push(f); }
33
+ else if ( art.kind === 'lattice' ) { px.library.loadLattice(art); report.lattice.push(f); } // version-gated, confluence-checked
34
+ else { report.skipped.push(f); continue; }
35
+ const man = art.manifest || {};
36
+ report.versions.push((man.name || f) + '@' + (man.version || '?'));
37
+ } catch ( e ) { report.skipped.push(f + ' (' + e.message + ')'); }
38
+ }
39
+ report.versions.sort();
40
+ return report;
41
+ }
42
+
43
+ /**
44
+ * Build the appliance (no socket yet — start() opens it).
45
+ * The escalation is either an N-tier ROUTER or a single frontier chat (give ONE of):
46
+ * @param cfg.tiers [{ name, ask|backend, egressClass? }] — ordered answer tiers (local/mid/frontier);
47
+ * a tier is a ready chat `ask` or an R0 `backend` preset spec (built via Graph.backends).
48
+ * @param cfg.policy { dataPolicy:'no-egress'(default)|'allow-mid'|'allow-all', order?:[name] } — the RGPD ceiling.
49
+ * @param cfg.frontierChat a single async ({system,user,maxTokens,temperature}) -> text — the escalation truth
50
+ * (legacy shorthand; treated as one allow-all frontier tier).
51
+ * @param cfg.onRoute optional ({query,tier}) -> void — provenance of which tier answered.
52
+ * @param cfg.localAsk optional small-model chat → semantic coverage (paraphrases hit the stock).
53
+ * @param cfg.store stock file (durable cross-restart). cfg.sgcDir the local SGC room (default ./sgc).
54
+ * @param cfg.port/host endpoint bind (default 4747 / 127.0.0.1 — LOCAL by default, on purpose).
55
+ */
56
+ /** A routing tier spec -> {name, ask, egressClass}. Either a ready chat `ask`, or an R0 backend preset
57
+ * built via `Graph.backends` (a backend's `egress:false` maps to the 'none' class unless egressClass is
58
+ * set). This is where R0 (config) and the routing (policy) converge. */
59
+ function resolveTier( t ) {
60
+ if ( typeof t.ask === 'function' ) return { name: t.name, ask: t.ask, egressClass: t.egressClass, egress: t.egress };
61
+ if ( t.backend ) {
62
+ const d = Graph.backends.resolveBackend(t.backend);
63
+ return { name: t.name || d.name, ask: Graph.backends.makeBackend(t.backend), egressClass: t.egressClass, egress: d.egress };
64
+ }
65
+ throw new Error('routing tier "' + (t.name || '?') + '" needs an `ask` or a `backend` spec');
66
+ }
67
+
68
+ /** Build the escalation ROUTER from cfg: an explicit N-tier list (`cfg.tiers` governed by `cfg.policy`,
69
+ * default dataPolicy no-egress) generalizing the C6 ladder, OR the legacy single chat backend
70
+ * (`cfg.frontierChat`) treated as an allow-all frontier tier. Either way the proxy sees one frontierAsk. */
71
+ function buildRouter( cfg ) {
72
+ const { makeRouter } = require('./routing.js');
73
+ let tiers, policy;
74
+ if ( Array.isArray(cfg.tiers) && cfg.tiers.length ) {
75
+ tiers = cfg.tiers.map(resolveTier);
76
+ policy = cfg.policy;
77
+ } else if ( typeof cfg.frontierChat === 'function' ) {
78
+ tiers = [{ name: 'frontier', ask: cfg.frontierChat, egressClass: 'frontier' }];
79
+ policy = cfg.policy || { dataPolicy: 'allow-all' }; // a single declared frontier is reachable (today's behaviour)
80
+ } else {
81
+ throw new Error('createApp needs cfg.tiers (N-tier routing) or cfg.frontierChat (a single chat backend)');
82
+ }
83
+ return makeRouter({ tiers: tiers, policy: policy, onRoute: cfg.onRoute });
84
+ }
85
+
86
+ function createApp( cfg ) {
87
+ cfg = cfg || {};
88
+ const combos = Graph.combos;
89
+ const router = buildRouter(cfg); // the escalation: N-tier policy-governed, or a single frontier
90
+ const semantic = cfg.localAsk ? combos.makeLocalCoverage({ localAsk: cfg.localAsk }) : {};
91
+ const px = combos.createProxyCache(Object.assign({
92
+ frontierAsk: combos.makeFrontierAsk(router.ask),
93
+ store: cfg.store, retention: true
94
+ }, semantic));
95
+ const sgcDir = path.resolve(cfg.sgcDir || 'sgc');
96
+ let sgcVersion = ''; // 'name@version,…' of the loaded bundles — refreshed by every sync, served live (x-sg-sgc-version)
97
+
98
+ async function sync() {
99
+ const loaded = loadBundles(px, sgcDir); // (re)load the local room THROUGH the gates
100
+ sgcVersion = loaded.versions.join(',');
101
+ return { loaded };
102
+ }
103
+
104
+ // ops readout: up? which SGC bundles are loaded (stock freshness)? the routing posture (configured
105
+ // tiers, which are reachable under the policy, the dataPolicy ceiling). Safe to poll — GET, no key,
106
+ // no query content.
107
+ function health() {
108
+ const m = (px.metrics && px.metrics()) || {};
109
+ return {
110
+ status: 'ok',
111
+ policy: router.policy.dataPolicy,
112
+ tiers: { configured: router.tiers.map(( t ) => t.name), reachable: router.plan().map(( t ) => t.name) },
113
+ sgc: sgcVersion ? sgcVersion.split(',') : [],
114
+ stock: (m.stock && m.stock.size != null) ? m.stock.size : null
115
+ };
116
+ }
117
+
118
+ let srv = null;
119
+ return {
120
+ proxy: px, sync, health,
121
+ escalationAsk: router.ask, // the escalation chat, reusable by other surfaces (mcp critique tool)
122
+ sgcDir, // where the room bundles live (mcp assistant lanes re-read them)
123
+ start: async function ( onReady ) {
124
+ const first = await sync();
125
+ const base = createServeHandler({ proxy: px, model: cfg.model, onAnswer: cfg.onAnswer, sgcVersion: () => sgcVersion });
126
+ // the serve handler contract is PURE: (requestDescriptor) -> responseDescriptor (startServeServer
127
+ // owns the socket). Intercept GET /healthz, delegate everything else to the OpenAI handler.
128
+ const handler = function ( reqd ) {
129
+ if ( reqd.method === 'GET' && String(reqd.url || '').split('?')[0] === '/healthz' ) {
130
+ return { status: 200, headers: { 'content-type': 'application/json' }, body: health() };
131
+ }
132
+ return base(reqd);
133
+ };
134
+ srv = startServeServer({ handler, port: cfg.port != null ? cfg.port : 4747, host: cfg.host || '127.0.0.1',
135
+ onReady: () => onReady && onReady(first) });
136
+ return srv;
137
+ },
138
+ stop: function () { if ( srv ) srv.close(); }
139
+ };
140
+ }
141
+
142
+ module.exports = { createApp, loadBundles };
package/lib/rooms.js ADDED
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+ /**
3
+ * SGC rooms — LOCAL bundle management (the community model: no central catalog, no subscription — anyone
4
+ * builds, freezes, shares and imports their own `.sgc` mini-repos). A room is just a directory of
5
+ * self-verifiable bundles: each `.sgc`/`.json` carries its manifest, and `freeze` writes the companion
6
+ * dossier (`<base>.dossier.md`: sha256 + inventory) that makes the bundle a fixed, auditable reference.
7
+ *
8
+ * Everything content-bearing stays engine-gated: `import` DRY-LOADS the bundle through the same gates the
9
+ * appliance uses (a malformed or empty bundle is REJECTED, never written into the room), and the appliance
10
+ * itself re-loads the room through `loadBundles` (kind-dispatched, version-gated) at boot.
11
+ */
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const crypto = require('crypto');
15
+
16
+ const sha256 = ( buf ) => crypto.createHash('sha256').update(buf).digest('hex');
17
+ const baseOf = ( f ) => f.replace(/\.(json|sgc)$/, '');
18
+
19
+ /** Parse + shape-check one bundle file. → { art, kind, name, version, classes } — throws on a non-bundle. */
20
+ function inspectBundle( file ) {
21
+ const bytes = fs.readFileSync(file);
22
+ const art = JSON.parse(bytes.toString('utf8'));
23
+ if ( !art || art.format !== 'sgc' ) throw new Error('not an sgc bundle (format!=="sgc")');
24
+ const man = art.manifest || {};
25
+ let classes = [];
26
+ if ( art.kind === 'methods' ) {
27
+ // the same admission surface the assistant lanes use — throws on an EMPTY certified vocabulary
28
+ const { stockWiring } = require('skynet-graph/lib/sg/mcp.js');
29
+ classes = stockWiring(art).hints.certifiedShapes;
30
+ } else if ( art.kind === 'lattice' ) {
31
+ const keys = (art.registry && art.registry.keys) || {};
32
+ classes = Object.keys(keys).sort();
33
+ if ( !classes.length ) throw new Error('empty lattice bundle (no registry keys)');
34
+ } else {
35
+ throw new Error('unknown sgc kind "' + art.kind + '" (methods|lattice)');
36
+ }
37
+ return { art, kind: art.kind, name: man.name || baseOf(path.basename(file)), version: man.version || '?',
38
+ classes, sha256: sha256(bytes) };
39
+ }
40
+
41
+ /** Inventory of a room. → [{ file, kind, name, version, classes, sha256, frozen, dossier }] (invalid files flagged). */
42
+ function list( dir ) {
43
+ if ( !fs.existsSync(dir) ) return [];
44
+ const rows = [];
45
+ for ( const f of fs.readdirSync(dir).sort() ) {
46
+ if ( !/\.(json|sgc)$/.test(f) ) continue;
47
+ const full = path.join(dir, f);
48
+ const dossier = ['.dossier.md', '.md'].map(( e ) => baseOf(f) + e ).find(( d ) => fs.existsSync(path.join(dir, d)) );
49
+ try {
50
+ const b = inspectBundle(full);
51
+ rows.push({ file: f, kind: b.kind, name: b.name, version: b.version, classes: b.classes.length,
52
+ sha256: b.sha256, frozen: !!dossier, dossier: dossier || null });
53
+ } catch ( e ) { rows.push({ file: f, invalid: e.message }); }
54
+ }
55
+ return rows;
56
+ }
57
+
58
+ /** Import a bundle INTO the room — gate-checked first (fail-closed: a bad bundle is never written). */
59
+ function importBundle( dir, srcFile ) {
60
+ const b = inspectBundle(srcFile); // throws → nothing written
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ const dest = path.join(dir, path.basename(srcFile));
63
+ fs.copyFileSync(srcFile, dest);
64
+ return { file: path.basename(dest), kind: b.kind, name: b.name, version: b.version, classes: b.classes.length, sha256: b.sha256 };
65
+ }
66
+
67
+ /** Export a bundle (+ its dossier when present) OUT of the room. */
68
+ function exportBundle( dir, fileOrName, destDir ) {
69
+ const rows = list(dir).filter(( r ) => !r.invalid );
70
+ const row = rows.find(( r ) => r.file === fileOrName || r.name === fileOrName );
71
+ if ( !row ) throw new Error('bundle "' + fileOrName + '" not in the room (' + rows.map(( r ) => r.name ).join(', ') + ')');
72
+ fs.mkdirSync(destDir, { recursive: true });
73
+ fs.copyFileSync(path.join(dir, row.file), path.join(destDir, row.file));
74
+ const out = { exported: [row.file] };
75
+ if ( row.dossier ) { fs.copyFileSync(path.join(dir, row.dossier), path.join(destDir, row.dossier)); out.exported.push(row.dossier); }
76
+ return out;
77
+ }
78
+
79
+ /** Freeze a bundle: write its dossier (sha256 + inventory) — the fixed, auditable reference for sharing. */
80
+ function freeze( dir, fileOrName ) {
81
+ const rows = list(dir).filter(( r ) => !r.invalid );
82
+ const row = rows.find(( r ) => r.file === fileOrName || r.name === fileOrName );
83
+ if ( !row ) throw new Error('bundle "' + fileOrName + '" not in the room');
84
+ const b = inspectBundle(path.join(dir, row.file));
85
+ const dossierFile = baseOf(row.file) + '.dossier.md';
86
+ const md = [
87
+ '# ' + b.name + '@' + b.version + ' — dossier (frozen reference)',
88
+ '',
89
+ '- file: `' + row.file + '`',
90
+ '- kind: `' + b.kind + '`',
91
+ '- sha256: `' + b.sha256 + '`',
92
+ '- frozen: ' + new Date().toISOString(),
93
+ '',
94
+ '## ' + (b.kind === 'methods' ? 'Certified classes (' + b.classes.length + ')' : 'Registry keys (' + b.classes.length + ')'),
95
+ '',
96
+ ...b.classes.map(( c ) => '- `' + c + '`' ),
97
+ '',
98
+ 'A consumer verifies the bytes against the sha256 above; the appliance re-verifies the content',
99
+ 'through the engine gates at load. A bundle whose bytes no longer match its dossier is NOT the',
100
+ 'frozen reference anymore.',
101
+ ''
102
+ ].join('\n');
103
+ fs.writeFileSync(path.join(dir, dossierFile), md);
104
+ return { dossier: dossierFile, sha256: b.sha256, classes: b.classes.length };
105
+ }
106
+
107
+ module.exports = { list, importBundle, exportBundle, freeze, inspectBundle };
package/lib/routing.js ADDED
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+ /**
3
+ * routing — the C6 ladder generalized from 2 tiers to N, governed by a POLICY.
4
+ *
5
+ * The verified stock stays the proxy's job (0-call, exact/semantic — it never egresses). This router is
6
+ * the GENERALIZED FRONTIER: an ordered list of answer tiers, each a chat `ask`
7
+ * (`async ({system,user,maxTokens,temperature}) -> text`), tagged with an EGRESS CLASS:
8
+ *
9
+ * 'none' the answer never leaves the machine (embedded gguf / a LAN model declared non-egressing)
10
+ * 'mid' a hosted mid-quant tier (the subscription's mid model — leaves, but contracted)
11
+ * 'frontier' the user's own frontier provider (leaves, to the user's chosen vendor)
12
+ *
13
+ * `policy.dataPolicy` is the RGPD ceiling — which classes may answer:
14
+ * 'no-egress' (DEFAULT) -> ['none'] nothing leaves; only local answers
15
+ * 'allow-mid' -> ['none','mid'] up to the hosted mid tier
16
+ * 'allow-all' -> ['none','mid','frontier']
17
+ *
18
+ * The router walks the reachable tiers in preference order (policy.order, else declaration order) and
19
+ * returns the first that answers; an unavailable tier falls through to the next reachable one. If the
20
+ * policy leaves NO reachable tier (or every reachable tier fails), it is a typed NO_REACHABLE_TIER
21
+ * refusal — a query is NEVER silently sent to a forbidden tier. This is the enforcement side of the M2
22
+ * no-egress guarantee: the default policy calls nothing that egresses. Converges with R0 backends.js — a
23
+ * tier is just a named `ask` (build it with `Graph.backends.makeBackend`) + an egress class (a backend's
24
+ * `egress:false` maps to 'none').
25
+ */
26
+ const POLICY = {
27
+ 'no-egress': ['none'],
28
+ 'allow-mid': ['none', 'mid'],
29
+ 'allow-all': ['none', 'mid', 'frontier']
30
+ };
31
+
32
+ function err( message, code, cause ) { const e = new Error(message); e.code = code; if ( cause ) e.cause = cause; return e; }
33
+
34
+ // a tier's egress class: explicit wins; else inferred from an `egress` boolean (R0 convergence: a
35
+ // non-egressing backend is 'none', an egressing one defaults to 'frontier' — the most-restricted class).
36
+ function classOf( t ) {
37
+ if ( t.egressClass ) return t.egressClass;
38
+ if ( t.egress === false ) return 'none';
39
+ return 'frontier';
40
+ }
41
+
42
+ /**
43
+ * @param cfg.tiers [{ name, ask, egressClass?|egress? }] ordered answer tiers (each a chat ask).
44
+ * @param cfg.policy { dataPolicy='no-egress', order?:[name] }
45
+ * @param cfg.onRoute optional ({query,tier}) -> void — provenance of which tier answered.
46
+ */
47
+ function makeRouter( cfg ) {
48
+ cfg = cfg || {};
49
+ const tiers = (cfg.tiers || []).map(function ( t ) { return { name: t.name, ask: t.ask, egressClass: classOf(t) }; });
50
+ for ( const t of tiers ) if ( typeof t.ask !== 'function' ) throw err('routing tier "' + t.name + '" needs an ask function', 'BAD_TIER');
51
+ const policy = Object.assign({ dataPolicy: 'no-egress' }, cfg.policy);
52
+ const allowed = POLICY[policy.dataPolicy];
53
+ if ( !allowed ) throw err('unknown dataPolicy "' + policy.dataPolicy + '" — known: ' + Object.keys(POLICY).join(', '), 'BAD_POLICY');
54
+ const onRoute = cfg.onRoute;
55
+
56
+ // the reachable tiers, in preference order (policy.order by name, else declaration order — stable).
57
+ function plan() {
58
+ let ordered = tiers;
59
+ if ( Array.isArray(policy.order) ) {
60
+ const rank = function ( name ) { const i = policy.order.indexOf(name); return i === -1 ? policy.order.length : i; };
61
+ ordered = tiers.map(( t, i ) => [t, i]).sort(( a, b ) => (rank(a[0].name) - rank(b[0].name)) || (a[1] - b[1])).map(( x ) => x[0]);
62
+ }
63
+ return ordered.filter(( t ) => allowed.indexOf(t.egressClass) !== -1);
64
+ }
65
+
66
+ // walk the reachable tiers; first success wins, an unavailable tier falls through. Typed refusal if
67
+ // the policy leaves nothing reachable, or every reachable tier failed.
68
+ async function route( chatArgs ) {
69
+ const candidates = plan();
70
+ if ( !candidates.length ) throw err('no tier reachable under dataPolicy "' + policy.dataPolicy + '" — a forbidden tier is never called', 'NO_REACHABLE_TIER');
71
+ let last = null;
72
+ for ( const t of candidates ) {
73
+ try {
74
+ const answer = await t.ask(chatArgs);
75
+ if ( onRoute ) onRoute({ query: chatArgs && chatArgs.user, tier: t.name });
76
+ return { answer: answer, tier: t.name };
77
+ } catch ( e ) { last = e; }
78
+ }
79
+ throw err('every reachable tier failed under dataPolicy "' + policy.dataPolicy + '"', 'NO_REACHABLE_TIER', last);
80
+ }
81
+
82
+ // chat-shaped: returns just the answer text, so `makeFrontierAsk(router.ask)` wraps it as the proxy's frontier.
83
+ async function ask( chatArgs ) { return (await route(chatArgs)).answer; }
84
+
85
+ return { plan: plan, route: route, ask: ask, policy: policy,
86
+ tiers: tiers.map(function ( t ) { return { name: t.name, egressClass: t.egressClass }; }) };
87
+ }
88
+
89
+ module.exports = { makeRouter: makeRouter, POLICY: POLICY };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ /**
3
+ * serve-config — the bin's CLI/env -> routing config resolver, kept PURE (arg + env in, plain object out)
4
+ * so the `skynet-client serve` wiring is unit-testable without spawning a process.
5
+ *
6
+ * `--routing <file.json>` (or $SG_ROUTING) declares the N tiers + policy for the appliance:
7
+ * { "tiers": [ { "name":"local", "egressClass":"none", "backend": { "preset":"local", "modelPath":"…" } },
8
+ * { "name":"mid", "egressClass":"mid", "backend": { "preset":"custom", "base":"…", "key":"…" } },
9
+ * { "name":"frontier","egressClass":"frontier","backend": { "preset":"deepseek" } } ],
10
+ * "policy": { "dataPolicy":"no-egress", "order":["frontier","mid","local"] } }
11
+ * `--policy <no-egress|allow-mid|allow-all>` (or $SG_POLICY) overrides just the dataPolicy. Each tier's
12
+ * `backend` is an R0 preset spec (built by Graph.backends at createApp time). No routing config -> null,
13
+ * so the caller uses the legacy single-frontier path unchanged.
14
+ */
15
+ const fs = require('fs');
16
+
17
+ function readFlag( argv, name ) { const i = argv.indexOf('--' + name); return i !== -1 ? argv[i + 1] : undefined; }
18
+
19
+ /**
20
+ * @param argv process.argv.slice(2)-style array
21
+ * @param env an environment object (process.env)
22
+ * @returns { tiers, policy } when an N-tier routing config is present, else null.
23
+ */
24
+ function resolveRouting( argv, env ) {
25
+ argv = argv || [];
26
+ env = env || {};
27
+ const file = readFlag(argv, 'routing') || env.SG_ROUTING;
28
+ let tiers = null, policy = null;
29
+ if ( file ) {
30
+ const rc = JSON.parse(fs.readFileSync(file, 'utf8'));
31
+ tiers = rc.tiers || null;
32
+ policy = rc.policy || null;
33
+ }
34
+ if ( !tiers ) return null; // no tiers declared -> not a routing config; the bin uses the single frontier
35
+ const dp = readFlag(argv, 'policy') || env.SG_POLICY;
36
+ if ( dp ) policy = Object.assign({}, policy, { dataPolicy: dp });
37
+ return { tiers: tiers, policy: policy || undefined };
38
+ }
39
+
40
+ module.exports = { resolveRouting: resolveRouting };