knosky 0.6.3 → 0.7.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/CHANGELOG.md +149 -93
  2. package/CREDITS.md +14 -14
  3. package/LICENSE.md +76 -76
  4. package/LIMITATIONS.md +33 -23
  5. package/PRIVACY.md +30 -30
  6. package/README.md +170 -117
  7. package/SECURITY.md +78 -46
  8. package/action/post-comment.mjs +94 -89
  9. package/action.yml +62 -62
  10. package/bin/knosky.mjs +279 -105
  11. package/core/CONTRACT.md +70 -70
  12. package/core/append-only-checkpoint.mjs +215 -0
  13. package/core/audit-writer.mjs +317 -0
  14. package/core/benchmark-results.mjs +225 -225
  15. package/core/bundle.mjs +178 -178
  16. package/core/churn.mjs +23 -23
  17. package/core/ci.mjs +268 -268
  18. package/core/comparison.mjs +189 -189
  19. package/core/config.mjs +189 -189
  20. package/core/constants.mjs +13 -13
  21. package/core/contract.mjs +123 -123
  22. package/core/cross-repo.mjs +111 -111
  23. package/core/decision-codes.mjs +92 -0
  24. package/core/destination.mjs +161 -161
  25. package/core/district-classification.mjs +111 -0
  26. package/core/doctor-scorecard.mjs +369 -0
  27. package/core/domain-store.mjs +347 -0
  28. package/core/edges.mjs +43 -43
  29. package/core/escalate.mjs +68 -68
  30. package/core/freshness.mjs +198 -194
  31. package/core/fs-indexer.mjs +218 -218
  32. package/core/key-store.mjs +348 -348
  33. package/core/layout.mjs +46 -46
  34. package/core/ledger.mjs +176 -141
  35. package/core/local-ipc-identity.mjs +500 -0
  36. package/core/lod.mjs +155 -155
  37. package/core/mode-b.mjs +410 -0
  38. package/core/multi-model-benchmark.mjs +405 -405
  39. package/core/net-lockdown.mjs +421 -0
  40. package/core/onboarding.mjs +223 -223
  41. package/core/operator-auth.mjs +317 -0
  42. package/core/overlays.mjs +45 -45
  43. package/core/policy-lattice.mjs +142 -0
  44. package/core/pr-comment.mjs +198 -198
  45. package/core/protocol-spec.mjs +460 -460
  46. package/core/provenance.mjs +320 -0
  47. package/core/retrieve.mjs +63 -63
  48. package/core/route.mjs +304 -304
  49. package/core/schema.mjs +275 -275
  50. package/core/signing-tiers.mjs +1265 -0
  51. package/core/swarm-bench.mjs +106 -0
  52. package/core/swarm-coordinator.mjs +867 -0
  53. package/core/trust-root-rekey.mjs +410 -0
  54. package/mcp/server.mjs +264 -108
  55. package/package.json +56 -46
  56. package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
  57. package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
  58. package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
  59. package/renderer/art/kenney/sheet_allCars.xml +545 -545
  60. package/renderer/build-rich.mjs +43 -43
  61. package/renderer/city.template.html +808 -808
  62. package/ssot/decision-codes.json +133 -0
  63. package/ssot/ladder-l0-l3.md +232 -0
  64. package/ssot/tool-menu.json +130 -0
@@ -0,0 +1,111 @@
1
+ // KnoSky district classification model, loader, and validator (SAT-505 / KS2-F1-2).
2
+ //
3
+ // Defines the five classification levels for KnoSky district nodes, ordered
4
+ // from least to most restrictive. Any node whose `district_class` field is
5
+ // absent, null, or unrecognised resolves to the most-restrictive level
6
+ // (BLOCKED) — fail-closed by design.
7
+ //
8
+ // Pure module: no I/O, no external dependencies, safe to import anywhere.
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Classification level constants
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Least-restrictive level — visible to anyone. */
15
+ export const CLASS_PUBLIC = 'public';
16
+
17
+ /** Visible within the organisation but not externally. */
18
+ export const CLASS_INTERNAL = 'internal';
19
+
20
+ /** Access limited to specific roles or teams. */
21
+ export const CLASS_RESTRICTED = 'restricted';
22
+
23
+ /** Highly sensitive — tightly controlled access. */
24
+ export const CLASS_CONFIDENTIAL = 'confidential';
25
+
26
+ /** Most-restrictive level — node is blocked from general access. */
27
+ export const CLASS_BLOCKED = 'blocked';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Ordered set and default
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * All valid classification levels, ordered from least to most restrictive.
35
+ * Index 0 is the least restrictive; the last index is the most restrictive.
36
+ * @type {readonly string[]}
37
+ */
38
+ export const CLASSES = Object.freeze([
39
+ CLASS_PUBLIC,
40
+ CLASS_INTERNAL,
41
+ CLASS_RESTRICTED,
42
+ CLASS_CONFIDENTIAL,
43
+ CLASS_BLOCKED,
44
+ ]);
45
+
46
+ /**
47
+ * The default classification applied when a node carries no recognisable
48
+ * `district_class` value. Fail-closed: most restrictive.
49
+ * @type {string}
50
+ */
51
+ export const DEFAULT_CLASS = CLASS_BLOCKED;
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // isValidClass
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /**
58
+ * Return `true` if `cls` is a recognised classification level string.
59
+ *
60
+ * @param {unknown} cls
61
+ * @returns {boolean}
62
+ */
63
+ export function isValidClass(cls) {
64
+ return CLASSES.includes(cls);
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // validateClass
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Validate a classification level string.
73
+ * Returns `{ ok: true }` for valid values; `{ ok: false, error: string }`
74
+ * for anything else.
75
+ *
76
+ * @param {unknown} cls
77
+ * @returns {{ ok: boolean, error?: string }}
78
+ */
79
+ export function validateClass(cls) {
80
+ if (isValidClass(cls)) return { ok: true };
81
+ return {
82
+ ok: false,
83
+ error: `district_class must be one of ${JSON.stringify(CLASSES)}, got: ${JSON.stringify(cls)}`,
84
+ };
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // loadClass
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Resolve the district classification of a node.
93
+ *
94
+ * Reads `node.district_class`. If the value is a recognised classification
95
+ * string it is returned as-is. Otherwise — absent, null, undefined, or any
96
+ * unrecognised string — the most-restrictive default ({@link DEFAULT_CLASS})
97
+ * is returned so that unknown nodes never escape to a permissive class.
98
+ *
99
+ * The function always returns a non-null string from {@link CLASSES}.
100
+ *
101
+ * @param {object} node Any node-like object (may carry a `district_class` field).
102
+ * @returns {string} One of the {@link CLASSES} values.
103
+ */
104
+ export function loadClass(node) {
105
+ if (node !== null && typeof node === 'object') {
106
+ // Preferred field: district_class. Accept short alias `classification` used by some indexes.
107
+ const cls = node.district_class ?? node.classification;
108
+ if (isValidClass(cls)) return cls;
109
+ }
110
+ return DEFAULT_CLASS;
111
+ }
@@ -0,0 +1,369 @@
1
+ // KnoSky doctor scorecard — Mode B + L0–L3 ladder + L3 foundation (DEC-106/108/110/113).
2
+ // Honest, plain-English lines. Never claims Windows kernel lockdown when unsupported.
3
+ // Pure ESM, Node stdlib.
4
+
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { platform } from 'node:os';
9
+ import { probeNetworkLockdownSupport, doctorLines as lockdownDoctorLines } from './net-lockdown.mjs';
10
+ import { resolveDomainRoot, loadDomain } from './domain-store.mjs';
11
+ import { closedSet, CODES } from './decision-codes.mjs';
12
+ import { verifyAuditChain } from './audit-writer.mjs';
13
+ import { DEFAULT_SWARM_QUOTAS, swarmPaths, readSwarmHeatmap } from './swarm-coordinator.mjs';
14
+
15
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
16
+
17
+ /**
18
+ * @typedef {object} ScoreRow
19
+ * @property {string} id
20
+ * @property {'ok'|'warn'|'fail'|'info'} level
21
+ * @property {string} title
22
+ * @property {string} detail
23
+ */
24
+
25
+ function mark(level, id, title, detail) {
26
+ return { id, level, title, detail };
27
+ }
28
+
29
+ function emoji(level) {
30
+ if (level === 'ok') return '🟢';
31
+ if (level === 'warn') return '🟡';
32
+ if (level === 'fail') return '🔴';
33
+ return '⚪';
34
+ }
35
+
36
+ /**
37
+ * Load SSOT artifacts from repo root.
38
+ */
39
+ export function inspectSsot(repoRoot = ROOT) {
40
+ const menuPath = join(repoRoot, 'ssot', 'tool-menu.json');
41
+ const codesPath = join(repoRoot, 'ssot', 'decision-codes.json');
42
+ const ladderPath = join(repoRoot, 'ssot', 'ladder-l0-l3.md');
43
+ const out = {
44
+ menuPath,
45
+ codesPath,
46
+ ladderPath,
47
+ menuOk: false,
48
+ codesOk: false,
49
+ ladderOk: false,
50
+ tier1: [],
51
+ closed: [],
52
+ errors: [],
53
+ };
54
+ try {
55
+ if (!existsSync(menuPath)) throw new Error('missing tool-menu.json');
56
+ const menu = JSON.parse(readFileSync(menuPath, 'utf8'));
57
+ out.menuOk = true;
58
+ out.tier1 = (menu.tiers?.tier1_governed?.tools || []).map((t) => t.name);
59
+ } catch (e) {
60
+ out.errors.push(String(e.message || e));
61
+ }
62
+ try {
63
+ if (!existsSync(codesPath)) throw new Error('missing decision-codes.json');
64
+ const codes = JSON.parse(readFileSync(codesPath, 'utf8'));
65
+ out.codesOk = true;
66
+ out.closed = codes.closed_set || [];
67
+ } catch (e) {
68
+ out.errors.push(String(e.message || e));
69
+ }
70
+ out.ladderOk = existsSync(ladderPath);
71
+ return out;
72
+ }
73
+
74
+ /**
75
+ * Inspect MCP server source for DEC-108 registration (static honesty).
76
+ */
77
+ export function inspectMcpMenu(repoRoot = ROOT) {
78
+ const serverPath = join(repoRoot, 'mcp', 'server.mjs');
79
+ const src = existsSync(serverPath) ? readFileSync(serverPath, 'utf8') : '';
80
+ const need = ['kc_search', 'kc_route', 'kc_bundle', 'kc_policy_check', 'createModeBDoor'];
81
+ const missing = need.filter((t) => !src.includes(t));
82
+ return {
83
+ serverPath,
84
+ present: existsSync(serverPath),
85
+ missing,
86
+ hasModeB: src.includes('createModeBDoor'),
87
+ hasSecurityAudit: src.includes('kc_audit_query'),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Build structured doctor scorecard.
93
+ * @param {object} [opts]
94
+ * @param {string} [opts.domainRoot]
95
+ * @param {string} [opts.cityPath]
96
+ * @param {string} [opts.repoRoot]
97
+ */
98
+ export function buildDoctorScorecard(opts = {}) {
99
+ const repoRoot = opts.repoRoot || ROOT;
100
+ const domainRoot = resolveDomainRoot(opts.cityPath, opts.domainRoot);
101
+ /** @type {ScoreRow[]} */
102
+ const rows = [];
103
+
104
+ // --- Platform / F0.5 ---
105
+ const lock = probeNetworkLockdownSupport();
106
+ const plat = platform();
107
+ if (lock.active) {
108
+ rows.push(mark('ok', 'F0.5', 'Network lockdown', 'ACTIVE — process is sandboxed for egress.'));
109
+ } else if (lock.supported && lock.toolAvailable) {
110
+ rows.push(
111
+ mark(
112
+ 'warn',
113
+ 'F0.5',
114
+ 'Network lockdown',
115
+ 'Available but NOT active for this process. External egress may still be possible until wrapped.',
116
+ ),
117
+ );
118
+ } else if (plat === 'win32') {
119
+ rows.push(
120
+ mark(
121
+ 'warn',
122
+ 'F0.5',
123
+ 'Network lockdown (Windows)',
124
+ 'Not kernel-enforced at runtime on Windows for this CLI. Do NOT claim "no egress guaranteed on Windows". Packaging/AppContainer would be required. ' +
125
+ (lock.unsupportedReason || lock.detail || ''),
126
+ ),
127
+ );
128
+ } else {
129
+ rows.push(
130
+ mark(
131
+ 'fail',
132
+ 'F0.5',
133
+ 'Network lockdown',
134
+ lock.unsupportedReason || lock.detail || 'unsupported',
135
+ ),
136
+ );
137
+ }
138
+
139
+ // --- SSOT / DEC-108 ---
140
+ const ssot = inspectSsot(repoRoot);
141
+ if (ssot.menuOk && ssot.codesOk && ssot.ladderOk) {
142
+ rows.push(
143
+ mark(
144
+ 'ok',
145
+ 'SSOT',
146
+ 'Protocol freeze files',
147
+ `tool-menu + decision-codes + ladder present. Tier1: ${ssot.tier1.join(', ') || '(none)'}`,
148
+ ),
149
+ );
150
+ } else {
151
+ rows.push(
152
+ mark(
153
+ 'fail',
154
+ 'SSOT',
155
+ 'Protocol freeze files',
156
+ `Incomplete SSOT under ${join(repoRoot, 'ssot')}: ${ssot.errors.join('; ') || 'ladder missing'}`,
157
+ ),
158
+ );
159
+ }
160
+
161
+ let codesRuntimeOk = true;
162
+ try {
163
+ const set = closedSet();
164
+ for (const c of ['ALLOW', 'DENY_IDENTITY', 'ADVISORY_UNAUTH', CODES.DENY_POLICY]) {
165
+ if (!set.includes(c)) codesRuntimeOk = false;
166
+ }
167
+ rows.push(
168
+ codesRuntimeOk
169
+ ? mark('ok', 'CODES', 'Decision codes runtime', `Closed set size ${set.length}; core codes present.`)
170
+ : mark('fail', 'CODES', 'Decision codes runtime', 'Closed set missing required codes.'),
171
+ );
172
+ } catch (e) {
173
+ rows.push(mark('fail', 'CODES', 'Decision codes runtime', String(e.message || e)));
174
+ }
175
+
176
+ const mcp = inspectMcpMenu(repoRoot);
177
+ if (mcp.present && mcp.missing.length === 0 && mcp.hasModeB) {
178
+ rows.push(
179
+ mark(
180
+ 'ok',
181
+ 'MCP',
182
+ 'Mode B front door wiring',
183
+ 'mcp/server.mjs registers Tier0+Tier1 and Mode B door. Defaults are non-authorizing until lease+policy ALLOW.',
184
+ ),
185
+ );
186
+ } else if (!mcp.present) {
187
+ rows.push(mark('fail', 'MCP', 'Mode B front door wiring', 'mcp/server.mjs missing'));
188
+ } else {
189
+ rows.push(
190
+ mark(
191
+ 'fail',
192
+ 'MCP',
193
+ 'Mode B front door wiring',
194
+ `Missing markers: ${mcp.missing.join(', ') || 'Mode B'}`,
195
+ ),
196
+ );
197
+ }
198
+
199
+ // --- Domain / Mode B readiness ---
200
+ try {
201
+ const domain = loadDomain(domainRoot);
202
+ const leaseN = domain.leaseStore.size;
203
+ const agentN = Object.keys(domain.agents || {}).length;
204
+ const requireId = domain.policy?.require_identity !== false;
205
+ rows.push(
206
+ mark(
207
+ leaseN > 0 ? 'ok' : 'warn',
208
+ 'MODEB-LEASES',
209
+ 'Local trust domain leases',
210
+ leaseN > 0
211
+ ? `Domain ${domainRoot}: ${agentN} agent(s), ${leaseN} lease(s). Mode B routes can bind identity.`
212
+ : `Domain ${domainRoot}: no leases yet. Run: knosky agent-register --agent <id>. Coding profile Mode B will DENY_IDENTITY until then (or use KC_PROFILE=advisory for labeled map-only). require_identity=${requireId}`,
213
+ ),
214
+ );
215
+
216
+ const audit = verifyAuditChain(domainRoot);
217
+ if (audit.ok) {
218
+ rows.push(
219
+ mark(
220
+ audit.count > 0 ? 'ok' : 'info',
221
+ 'MODEB-AUDIT',
222
+ 'Audit ledger chain',
223
+ audit.count > 0
224
+ ? `Hash chain OK (${audit.count} event(s)).`
225
+ : 'No audit events yet — chain OK (empty). First Mode B call will write receipts.',
226
+ ),
227
+ );
228
+ } else {
229
+ rows.push(
230
+ mark('fail', 'MODEB-AUDIT', 'Audit ledger chain', `Verify failed: ${audit.reason || 'unknown'} @${audit.at}`),
231
+ );
232
+ }
233
+ } catch (e) {
234
+ rows.push(mark('fail', 'MODEB-DOMAIN', 'Local trust domain', String(e.message || e)));
235
+ }
236
+
237
+ // --- L0–L3 ladder honesty ---
238
+ const claimTips = [];
239
+ claimTips.push('L0 local map: OK to claim when city is local-only and default install does not upload source.');
240
+ claimTips.push('L1 share-safe: OK only after share-safe indexer / secret scan path you actually ran.');
241
+ claimTips.push('L2 governed: OK only when Mode B ALLOW path has identity+policy+audit for that install.');
242
+ claimTips.push(
243
+ 'L3 swarm: FOUNDATION present (coordinator module). Do NOT market full production “swarm-safe fleet” until remaining polish/benchmarks/red-team gates land.',
244
+ );
245
+ rows.push(mark('info', 'LADDER', 'L0–L3 claim ceiling', claimTips.join(' | ')));
246
+
247
+ // --- L3 foundation ---
248
+ try {
249
+ const sp = swarmPaths(domainRoot);
250
+ const heat = readSwarmHeatmap(domainRoot);
251
+ const hasCoord = existsSync(join(repoRoot, 'core', 'swarm-coordinator.mjs'));
252
+ if (!hasCoord) {
253
+ rows.push(mark('fail', 'L3', 'Swarm coordinator module', 'core/swarm-coordinator.mjs missing'));
254
+ } else {
255
+ rows.push(
256
+ mark(
257
+ 'ok',
258
+ 'L3-MOD',
259
+ 'Swarm coordinator module',
260
+ `Foundation present. Default quotas: claims≤${DEFAULT_SWARM_QUOTAS.maxClaimsPerAgent}, actions/window≤${DEFAULT_SWARM_QUOTAS.maxActionsPerWindow}, antiProbe≥${DEFAULT_SWARM_QUOTAS.antiProbeDenyThreshold}. Fairness=FIFO wait when enableFifoWait.`,
261
+ ),
262
+ );
263
+ if (heat.ok) {
264
+ rows.push(
265
+ mark(
266
+ 'ok',
267
+ 'L3-HEAT',
268
+ 'Swarm heatmap',
269
+ `Readable at ${sp.heatmapPath}. Use: knosky swarm status`,
270
+ ),
271
+ );
272
+ } else {
273
+ rows.push(
274
+ mark(
275
+ 'info',
276
+ 'L3-HEAT',
277
+ 'Swarm heatmap',
278
+ `No heatmap yet under ${sp.heatmapPath}. Run knosky swarm status to write a fresh snapshot.`,
279
+ ),
280
+ );
281
+ }
282
+ }
283
+ } catch (e) {
284
+ rows.push(mark('warn', 'L3', 'Swarm coordinator', String(e.message || e)));
285
+ }
286
+
287
+ // --- Telemetry constitution ---
288
+ rows.push(
289
+ mark(
290
+ 'ok',
291
+ 'METRICS',
292
+ 'Telemetry posture',
293
+ 'Default: no always-on product telemetry. Adoption metrics = opt-in / survey / public benchmarks only (DEC-114).',
294
+ ),
295
+ );
296
+
297
+ // --- Packs presence ---
298
+ const packsRoot = join(repoRoot, 'packs');
299
+ if (existsSync(packsRoot)) {
300
+ rows.push(
301
+ mark(
302
+ 'ok',
303
+ 'PACKS',
304
+ 'Consumer packs tree',
305
+ 'packs/ present (Hermes/Claude/Cursor/Codex + P1 stubs). Run pack smoke tests before release.',
306
+ ),
307
+ );
308
+ } else {
309
+ rows.push(mark('warn', 'PACKS', 'Consumer packs tree', 'packs/ missing'));
310
+ }
311
+
312
+ const summary = {
313
+ ok: rows.filter((r) => r.level === 'ok').length,
314
+ warn: rows.filter((r) => r.level === 'warn').length,
315
+ fail: rows.filter((r) => r.level === 'fail').length,
316
+ info: rows.filter((r) => r.level === 'info').length,
317
+ };
318
+
319
+ return {
320
+ version: '1.0.0',
321
+ platform: plat,
322
+ domainRoot,
323
+ repoRoot,
324
+ lockdown: lock,
325
+ summary,
326
+ rows,
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Human lines for CLI `knosky doctor`.
332
+ * @param {object} [opts]
333
+ * @returns {string[]}
334
+ */
335
+ export function doctorScorecardLines(opts = {}) {
336
+ const card = buildDoctorScorecard(opts);
337
+ const lines = [];
338
+ lines.push('KnoSky doctor — Wave 1 scorecard (Mode B · ladder · L3 foundation · sandbox)');
339
+ lines.push('');
340
+ lines.push(
341
+ `Summary: ${card.summary.ok} ok · ${card.summary.warn} warn · ${card.summary.fail} fail · ${card.summary.info} info`,
342
+ );
343
+ lines.push(`Domain : ${card.domainRoot}`);
344
+ lines.push(`OS : ${card.platform}`);
345
+ lines.push('');
346
+
347
+ // Keep classic F0.5 lines for continuity
348
+ lines.push('--- F0.5 lockdown (classic) ---');
349
+ for (const l of lockdownDoctorLines()) lines.push(l);
350
+ lines.push('');
351
+
352
+ lines.push('--- Scorecard ---');
353
+ for (const r of card.rows) {
354
+ lines.push(`${emoji(r.level)} [${r.id}] ${r.title}`);
355
+ lines.push(` ${r.detail}`);
356
+ }
357
+ lines.push('');
358
+ lines.push('Hints:');
359
+ lines.push(' knosky agent-register --agent <id> # mint Mode B lease');
360
+ lines.push(' knosky swarm status # L3 heatmap snapshot');
361
+ lines.push(' KC_PROFILE=coding|advisory|security # MCP profile');
362
+ lines.push(' Claims: never call Mode A “authorized”; never claim Windows no-egress if scorecard warns.');
363
+ return lines;
364
+ }
365
+
366
+ /** CLI exit code: 0 if no fail rows, 2 if any fail. */
367
+ export function doctorExitCode(card) {
368
+ return card.summary.fail > 0 ? 2 : 0;
369
+ }