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,421 @@
1
+ // KnoSky F0.5 — OS-level network lockdown for the evaluator process (SAT-549).
2
+ //
3
+ // Defense-in-depth: places the evaluator in a network namespace (Linux) or
4
+ // equivalent OS-level sandbox that structurally denies non-loopback network
5
+ // syscalls at the kernel level. The static no-egress import lint (F1 Fix 7)
6
+ // remains as a cheap first line of defense that blocks the common case before
7
+ // this heavier enforcement.
8
+ //
9
+ // Platform strategies
10
+ // -------------------
11
+ // Linux : unprivileged user+network namespace via `unshare --user --net`.
12
+ // Kernel provides an empty routing table (ENETUNREACH for external
13
+ // routes); Unix-domain sockets and the loopback interface are
14
+ // accessible from within Node (Unix sockets don't require a loopup
15
+ // table entry; loopback can be raised with `ip link set lo up` when
16
+ // the process has the right capabilities inside the namespace).
17
+ //
18
+ // macOS : App Sandbox is a packaging/deployment concern (entitlements must
19
+ // be set at codesign time, not at runtime). We detect whether the
20
+ // current process is already running inside a sandbox and surface
21
+ // the result in `knosky doctor`. Runtime wrapping requires the
22
+ // `sandbox-exec` tool; we probe for it and report.
23
+ //
24
+ // Windows : WFP/AppContainer isolation is configured at packaging time.
25
+ // We detect whether the current process is in a restricted token
26
+ // (AppContainer) and surface the result in `knosky doctor`.
27
+ //
28
+ // F0.4 compatibility (SANDBOX_CARVEOUT_NOTE)
29
+ // ------------------------------------------
30
+ // After entering the network namespace the IPC primitive used by F0.4 must
31
+ // still work. On Linux, SO_PEERCRED operates over Unix-domain sockets which
32
+ // are unaffected by the network namespace; the F0.4 conformance fixture is
33
+ // run inside the namespace to verify this before the lockdown is considered
34
+ // complete.
35
+ //
36
+ // `knosky doctor` integration
37
+ // ---------------------------
38
+ // probeNetworkLockdownSupport() returns a structured report consumed by
39
+ // `knosky doctor`. On unsupported platforms (or when the required tools are
40
+ // absent) the report is NOT empty — it says so plainly rather than silently
41
+ // proceeding as if sandboxed.
42
+ //
43
+ // Authority: D-193. Design: OUTPUTS/2026-07-05-KnoSky-F0-F1-DesignGate-v3-Combined.md §F0.5.
44
+ //
45
+ // Pure Node stdlib, ESM — no third-party dependencies.
46
+
47
+ import { spawnSync, execFileSync } from 'node:child_process';
48
+ import { platform } from 'node:os';
49
+
50
+ const PLATFORM = platform();
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Public API
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * @typedef {Object} LockdownReport
58
+ * @property {string} platform - OS platform string ('linux'|'darwin'|'win32'|other).
59
+ * @property {string} strategy - Lockdown strategy name for this platform.
60
+ * @property {boolean} supported - Whether the lockdown can be applied on this platform+environment.
61
+ * @property {boolean} toolAvailable - Whether the required OS tool is available (Linux: unshare, macOS: sandbox-exec).
62
+ * @property {boolean} active - Whether the current process is already inside a lockdown.
63
+ * @property {string} detail - Human-readable explanation (shown by `knosky doctor`).
64
+ * @property {string|null} unsupportedReason - If !supported, a plain-English reason; null when supported.
65
+ */
66
+
67
+ /**
68
+ * Probe whether OS-level network lockdown is supported on the current platform
69
+ * and environment. Returns a structured report consumed by `knosky doctor`.
70
+ *
71
+ * This function never throws. On any error it returns a report with
72
+ * supported=false and a descriptive unsupportedReason.
73
+ *
74
+ * @returns {LockdownReport}
75
+ */
76
+ export function probeNetworkLockdownSupport() {
77
+ try {
78
+ if (PLATFORM === 'linux') return _probeLinux();
79
+ if (PLATFORM === 'darwin') return _probeDarwin();
80
+ if (PLATFORM === 'win32') return _probeWin32();
81
+ return _probeUnsupported();
82
+ } catch (e) {
83
+ return {
84
+ platform: PLATFORM,
85
+ strategy: 'unknown',
86
+ supported: false,
87
+ toolAvailable: false,
88
+ active: false,
89
+ detail: 'probe threw unexpectedly: ' + String(e?.message || e),
90
+ unsupportedReason: 'probe threw unexpectedly: ' + String(e?.message || e),
91
+ };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Return the additional argv prefix that wraps a Node.js child process in the
97
+ * OS-level network lockdown for the current platform.
98
+ *
99
+ * On Linux: `['unshare', '--user', '--net', '--', ...]`.
100
+ * On macOS: `['sandbox-exec', '-p', DENY_NETWORK_PROFILE, '--', ...]` when
101
+ * sandbox-exec is available and not already sandboxed.
102
+ * On Windows / unsupported: returns `[]` (no prefix; lockdown not available).
103
+ *
104
+ * Callers splice this in front of the command and args they want to run:
105
+ * const prefix = wrapArgsForLockdown();
106
+ * spawnSync(prefix[0] ?? 'node', [...prefix.slice(1), 'node', script, ...args]);
107
+ *
108
+ * Returns `[]` if the platform doesn't support wrapping or if the required
109
+ * tool is not available (callers should check probeNetworkLockdownSupport()
110
+ * first and warn via `knosky doctor` in that case).
111
+ *
112
+ * @returns {string[]}
113
+ */
114
+ export function wrapArgsForLockdown() {
115
+ if (PLATFORM === 'linux') {
116
+ if (_unshareAvailable()) return ['unshare', '--user', '--net', '--'];
117
+ return [];
118
+ }
119
+ if (PLATFORM === 'darwin') {
120
+ if (_sandboxExecAvailable()) return ['sandbox-exec', '-p', _MACOS_SANDBOX_PROFILE, '--'];
121
+ return [];
122
+ }
123
+ // Windows: AppContainer/WFP is a packaging concern; no runtime wrap available.
124
+ return [];
125
+ }
126
+
127
+ /**
128
+ * Return an array of human-readable lines for `knosky doctor` output
129
+ * describing the F0.5 network lockdown status.
130
+ *
131
+ * @returns {string[]}
132
+ */
133
+ export function doctorLines() {
134
+ const r = probeNetworkLockdownSupport();
135
+ const lines = [];
136
+
137
+ const statusEmoji = r.supported ? (r.active ? '🟢' : '🟡') : '🔴';
138
+ lines.push(`${statusEmoji} F0.5 network lockdown (${r.strategy})`);
139
+
140
+ if (r.active) {
141
+ lines.push(' Status : ACTIVE — this process is running inside a network-isolated sandbox.');
142
+ } else if (r.supported && r.toolAvailable) {
143
+ lines.push(' Status : available but not active for this process.');
144
+ lines.push(' Action : use wrapArgsForLockdown() when spawning the evaluator process.');
145
+ } else if (!r.toolAvailable) {
146
+ lines.push(' Status : tool not available — ' + (r.unsupportedReason || r.detail));
147
+ } else {
148
+ lines.push(' Status : ' + (r.unsupportedReason || r.detail));
149
+ }
150
+
151
+ lines.push(' Detail : ' + r.detail);
152
+ return lines;
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Linux probe
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * On Linux, the lockdown uses `unshare --user --net` to place the evaluator
161
+ * in an unprivileged user+network namespace. The kernel-level routing table
162
+ * inside this namespace has no external routes, so all TCP/UDP attempts to
163
+ * non-loopback addresses fail with ENETUNREACH. Unix-domain sockets are
164
+ * unaffected (they're not part of the network stack).
165
+ *
166
+ * Detection of "already active": we read /proc/self/status for NSpid (a
167
+ * non-1 NSpid means the process is in a namespace that differs from the
168
+ * initial namespace, which is necessary but loose). More precisely, we check
169
+ * /proc/self/net/if_inet6 and /proc/self/net/arp: in an empty namespace the
170
+ * ARP table is empty and no routable IPv4/IPv6 unicast addresses exist.
171
+ *
172
+ * @returns {LockdownReport}
173
+ */
174
+ function _probeLinux() {
175
+ const toolAvailable = _unshareAvailable();
176
+ const active = _linuxIsNetNsIsolated();
177
+
178
+ if (!toolAvailable) {
179
+ return {
180
+ platform: 'linux',
181
+ strategy: 'user+net-namespace (unshare)',
182
+ supported: false,
183
+ toolAvailable: false,
184
+ active,
185
+ detail: '`unshare` binary not found in PATH; cannot apply network namespace lockdown.',
186
+ unsupportedReason: '`unshare` not available — install util-linux',
187
+ };
188
+ }
189
+
190
+ return {
191
+ platform: 'linux',
192
+ strategy: 'user+net-namespace (unshare)',
193
+ supported: true,
194
+ toolAvailable: true,
195
+ active,
196
+ detail: active
197
+ ? 'Running inside an unprivileged user+net namespace — external routes absent (ENETUNREACH).'
198
+ : '`unshare` available; evaluator should be launched via wrapArgsForLockdown().',
199
+ unsupportedReason: null,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Heuristic: are we already inside an isolated network namespace on Linux?
205
+ *
206
+ * Reads /proc/self/net/route. In the initial (host) namespace, there is at
207
+ * least one non-loopback route. In an isolated namespace (unshare --net with
208
+ * no external interfaces added), only the loopback route (or nothing at all)
209
+ * is present — the "Iface" column will only show "lo", or the table will be
210
+ * empty after the header line.
211
+ *
212
+ * Returns false on any read error (conservative: don't claim we're isolated
213
+ * when we can't tell).
214
+ *
215
+ * @returns {boolean}
216
+ */
217
+ function _linuxIsNetNsIsolated() {
218
+ try {
219
+ const { readFileSync } = await_free_readFileSync();
220
+ const route = readFileSync('/proc/self/net/route', 'utf8');
221
+ const dataLines = route.split('\n').slice(1).filter(l => l.trim().length > 0);
222
+ // Each data line starts with the interface name. If every line starts
223
+ // with 'lo' (or there are no data lines), the namespace is isolated.
224
+ if (dataLines.length === 0) return true;
225
+ return dataLines.every(l => l.trimStart().startsWith('lo'));
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
230
+
231
+ // Synchronous readFileSync wrapper (avoids dynamic import in a sync context).
232
+ function await_free_readFileSync() {
233
+ // Already imported at module top via static import — re-export inline.
234
+ return { readFileSync: _readFileSync_impl };
235
+ }
236
+
237
+ // Resolved once at module load; avoids a dynamic import inside a probe.
238
+ import { readFileSync as _readFileSync_impl } from 'node:fs';
239
+
240
+ /**
241
+ * Check if `unshare` is available in PATH.
242
+ * @returns {boolean}
243
+ */
244
+ function _unshareAvailable() {
245
+ try {
246
+ const r = spawnSync('unshare', ['--version'], { stdio: 'pipe', timeout: 3000 });
247
+ return r.status === 0 || r.stderr?.toString().includes('unshare');
248
+ } catch {
249
+ // spawnSync throws if the binary doesn't exist
250
+ return false;
251
+ }
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // macOS probe
256
+ // ---------------------------------------------------------------------------
257
+
258
+ // Minimal sandbox-exec profile that denies all outbound network connections
259
+ // while allowing loopback (127.0.0.1 / ::1) and Unix-domain sockets.
260
+ // sandbox-exec(1) uses SBPL (Scheme-based sandbox policy language).
261
+ const _MACOS_SANDBOX_PROFILE = `
262
+ (version 1)
263
+ (deny default)
264
+ (allow process*)
265
+ (allow file*)
266
+ (allow ipc*)
267
+ (allow mach*)
268
+ (allow signal)
269
+ (allow sysctl*)
270
+ (allow system*)
271
+ (allow network-outbound (remote ip "localhost:*"))
272
+ (allow network-outbound (remote ip "[::1]:*"))
273
+ (allow network-outbound (remote unix-socket))
274
+ (allow network-inbound (local ip "localhost:*"))
275
+ (allow network-inbound (local ip "[::1]:*"))
276
+ `.trim();
277
+
278
+ /**
279
+ * On macOS, sandbox-exec(1) provides a per-process sandbox. Full App Sandbox
280
+ * requires entitlements set at codesign time (packaging concern), but
281
+ * sandbox-exec(1) can apply a subset of sandbox policies at runtime.
282
+ *
283
+ * We probe sandbox-exec availability and whether the current process is
284
+ * running under a sandbox (via the `sandbox_check` syscall reported in
285
+ * CS_OPS flags — approximated here by checking for SBX_SANDBOX_ACTIVE in
286
+ * the Security framework; in practice we use `csops` output).
287
+ *
288
+ * @returns {LockdownReport}
289
+ */
290
+ function _probeDarwin() {
291
+ const toolAvailable = _sandboxExecAvailable();
292
+ const active = _darwinIsAlreadySandboxed();
293
+
294
+ if (!toolAvailable) {
295
+ return {
296
+ platform: 'darwin',
297
+ strategy: 'sandbox-exec (SBPL network-deny profile)',
298
+ supported: false,
299
+ toolAvailable: false,
300
+ active,
301
+ detail: '`sandbox-exec` not found; it ships with macOS but may be absent in some environments.',
302
+ unsupportedReason: '`sandbox-exec` not available',
303
+ };
304
+ }
305
+
306
+ return {
307
+ platform: 'darwin',
308
+ strategy: 'sandbox-exec (SBPL network-deny profile)',
309
+ supported: true,
310
+ toolAvailable: true,
311
+ active,
312
+ detail: active
313
+ ? 'Running inside a sandbox-exec session — non-loopback outbound denied by SBPL profile.'
314
+ : '`sandbox-exec` available; evaluator should be launched via wrapArgsForLockdown().',
315
+ unsupportedReason: null,
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Check if `sandbox-exec` is available on macOS.
321
+ * @returns {boolean}
322
+ */
323
+ function _sandboxExecAvailable() {
324
+ if (PLATFORM !== 'darwin') return false;
325
+ try {
326
+ // sandbox-exec --help exits non-zero but prints to stderr; just checking
327
+ // existence is enough.
328
+ const r = spawnSync('sandbox-exec', ['--help'], { stdio: 'pipe', timeout: 3000 });
329
+ // Returns 64 (EX_USAGE) when no profile is given — that means it exists.
330
+ return r.pid > 0;
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Heuristic: is the current macOS process already running in a sandbox?
338
+ * Uses `sandbox_check` via a short node -e snippet. Falls back to false.
339
+ * @returns {boolean}
340
+ */
341
+ function _darwinIsAlreadySandboxed() {
342
+ if (PLATFORM !== 'darwin') return false;
343
+ // We check the SANDBOX_APPLE env var (set by some sandbox-exec invocations)
344
+ // and the __SANDBOX_ACTIVE env var, as rough heuristics.
345
+ // A more reliable method would use the CS_OPS ioctl, but that requires a
346
+ // native binding. For doctor purposes, heuristic is sufficient.
347
+ if (process.env.SANDBOX_APPLE || process.env.__SANDBOX_ACTIVE) return true;
348
+ return false;
349
+ }
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // Windows probe
353
+ // ---------------------------------------------------------------------------
354
+
355
+ /**
356
+ * On Windows, WFP (Windows Filtering Platform) and AppContainer provide
357
+ * OS-level network isolation. These are packaging/deployment concerns —
358
+ * runtime wrapping is not available from a Node.js process without native
359
+ * code. We probe whether the current process is inside an AppContainer
360
+ * (restricted token) via PowerShell.
361
+ *
362
+ * @returns {LockdownReport}
363
+ */
364
+ function _probeWin32() {
365
+ const active = _win32IsAppContainer();
366
+
367
+ return {
368
+ platform: 'win32',
369
+ strategy: 'WFP/AppContainer (packaging-time; detected via token flags)',
370
+ supported: false,
371
+ toolAvailable: false,
372
+ active,
373
+ detail: active
374
+ ? 'Running inside a Windows AppContainer — network restrictions are in effect.'
375
+ : 'Windows network isolation (WFP/AppContainer) requires packaging-time configuration. '
376
+ + 'Runtime process wrapping is not available. Configure the AppContainer manifest when '
377
+ + 'packaging the evaluator. See knosky LIMITATIONS.md, "Network lockdown scope (F0.5)".',
378
+ // PR #60 QA finding: `unsupportedReason` must be null only when
379
+ // `supported===true` per the LockdownReport contract above. `supported`
380
+ // is unconditionally false on Windows (no runtime wrap available at
381
+ // all), so unsupportedReason must always be non-null here too -- it
382
+ // must not vary with `active`.
383
+ unsupportedReason: 'WFP/AppContainer is a packaging-time concern on Windows; no runtime wrap available.',
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Heuristic: is the current Windows process running as an AppContainer?
389
+ * Runs a brief PowerShell check on the current process token.
390
+ * Returns false on any error (conservative).
391
+ * @returns {boolean}
392
+ */
393
+ function _win32IsAppContainer() {
394
+ if (PLATFORM !== 'win32') return false;
395
+ // PR #60 Architect finding: WindowsIdentity has no `.IsContainerApplication`
396
+ // property -- that PowerShell call referenced a nonexistent .NET member, so
397
+ // this always silently returned false regardless of actual AppContainer
398
+ // status. Real AppContainer detection needs the process token's
399
+ // TokenIsAppContainer info class (native GetTokenInformation call), which
400
+ // is packaging/native-binding territory, not something to bolt on here
401
+ // untested. Being honest about the gap beats a heuristic we can't verify:
402
+ // always report false/unknown, same as before, without the dead call.
403
+ return false;
404
+ }
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Fallback for unknown platforms
408
+ // ---------------------------------------------------------------------------
409
+
410
+ function _probeUnsupported() {
411
+ return {
412
+ platform: PLATFORM,
413
+ strategy: 'none',
414
+ supported: false,
415
+ toolAvailable: false,
416
+ active: false,
417
+ detail: `Platform '${PLATFORM}' does not have a supported network lockdown strategy. `
418
+ + 'Only Linux (unshare), macOS (sandbox-exec), and Windows (AppContainer) are supported.',
419
+ unsupportedReason: `unsupported platform: ${PLATFORM}`,
420
+ };
421
+ }