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.
- package/CHANGELOG.md +149 -93
- package/CREDITS.md +14 -14
- package/LICENSE.md +76 -76
- package/LIMITATIONS.md +33 -23
- package/PRIVACY.md +30 -30
- package/README.md +170 -117
- package/SECURITY.md +78 -46
- package/action/post-comment.mjs +94 -89
- package/action.yml +62 -62
- package/bin/knosky.mjs +279 -105
- package/core/CONTRACT.md +70 -70
- package/core/append-only-checkpoint.mjs +215 -0
- package/core/audit-writer.mjs +317 -0
- package/core/benchmark-results.mjs +225 -225
- package/core/bundle.mjs +178 -178
- package/core/churn.mjs +23 -23
- package/core/ci.mjs +268 -268
- package/core/comparison.mjs +189 -189
- package/core/config.mjs +189 -189
- package/core/constants.mjs +13 -13
- package/core/contract.mjs +123 -123
- package/core/cross-repo.mjs +111 -111
- package/core/decision-codes.mjs +92 -0
- package/core/destination.mjs +161 -161
- package/core/district-classification.mjs +111 -0
- package/core/doctor-scorecard.mjs +369 -0
- package/core/domain-store.mjs +347 -0
- package/core/edges.mjs +43 -43
- package/core/escalate.mjs +68 -68
- package/core/freshness.mjs +198 -194
- package/core/fs-indexer.mjs +218 -218
- package/core/key-store.mjs +348 -348
- package/core/layout.mjs +46 -46
- package/core/ledger.mjs +176 -141
- package/core/local-ipc-identity.mjs +500 -0
- package/core/lod.mjs +155 -155
- package/core/mode-b.mjs +410 -0
- package/core/multi-model-benchmark.mjs +405 -405
- package/core/net-lockdown.mjs +421 -0
- package/core/onboarding.mjs +223 -223
- package/core/operator-auth.mjs +317 -0
- package/core/overlays.mjs +45 -45
- package/core/policy-lattice.mjs +142 -0
- package/core/pr-comment.mjs +198 -198
- package/core/protocol-spec.mjs +460 -460
- package/core/provenance.mjs +320 -0
- package/core/retrieve.mjs +63 -63
- package/core/route.mjs +304 -304
- package/core/schema.mjs +275 -275
- package/core/signing-tiers.mjs +1265 -0
- package/core/swarm-bench.mjs +106 -0
- package/core/swarm-coordinator.mjs +867 -0
- package/core/trust-root-rekey.mjs +410 -0
- package/mcp/server.mjs +264 -108
- package/package.json +56 -46
- package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
- package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
- package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
- package/renderer/art/kenney/sheet_allCars.xml +545 -545
- package/renderer/build-rich.mjs +43 -43
- package/renderer/city.template.html +808 -808
- package/ssot/decision-codes.json +133 -0
- package/ssot/ladder-l0-l3.md +232 -0
- 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
|
+
}
|