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,500 @@
|
|
|
1
|
+
// KnoSky F0.4 — Local authentication: kernel-verified principal identity (SAT-548).
|
|
2
|
+
//
|
|
3
|
+
// Establishes the OS-kernel-verified identity of the process on the other end of a
|
|
4
|
+
// local IPC connection. NEVER relies on a self-asserted field from the request
|
|
5
|
+
// payload — identity comes exclusively from the kernel via:
|
|
6
|
+
//
|
|
7
|
+
// Linux : SO_PEERCRED (getsockopt over a Unix-domain socket)
|
|
8
|
+
// macOS : LOCAL_PEERCRED (getsockopt) or xpc_connection_get_audit_token
|
|
9
|
+
// Windows : GetNamedPipeClientProcessId + OpenProcessToken over a named pipe
|
|
10
|
+
//
|
|
11
|
+
// Two-layer clarification from the ticket (D-193/D-194):
|
|
12
|
+
// F0.4 proves "real local OS process" (pid/uid/gid from the kernel).
|
|
13
|
+
// Swarm-lease binding (TRD §2.8) is a separate layer that maps a kernel-verified
|
|
14
|
+
// process to a named agent role — that is `resolveLeaseIdentity()` below.
|
|
15
|
+
//
|
|
16
|
+
// Lease-identity fix (GLM, round-3): the evaluator NEVER trusts `agentId` from
|
|
17
|
+
// the request payload. It looks up the lease record server-side by `leaseId`;
|
|
18
|
+
// the record's own `agentId` is authoritative. The payload copy is checked
|
|
19
|
+
// against the record (mismatch → reject) but never accepted as truth on its own.
|
|
20
|
+
//
|
|
21
|
+
// F0.4↔F0.5 interaction: each platform sandbox profile (F0.5) must ship a
|
|
22
|
+
// dedicated conformance fixture that exercises getPeerIdentity() from inside that
|
|
23
|
+
// specific sandbox before the profile is considered complete. If a sandbox
|
|
24
|
+
// profile blocks the IPC primitive a narrow, logged carve-out is emitted instead
|
|
25
|
+
// of a general sandbox loosening (see SANDBOX_CARVEOUT_NOTE below).
|
|
26
|
+
//
|
|
27
|
+
// Pure Node stdlib, ESM — no third-party dependencies.
|
|
28
|
+
// Requires a C compiler (cc/gcc/clang) at first call on Linux/macOS for the
|
|
29
|
+
// helper binary; the path is cached process-wide after first compile.
|
|
30
|
+
|
|
31
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
32
|
+
import { writeFileSync, mkdtempSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
33
|
+
import { join, dirname } from 'node:path';
|
|
34
|
+
import { tmpdir, platform } from 'node:os';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
|
|
37
|
+
const PLATFORM = platform();
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// SANDBOX_CARVEOUT_NOTE
|
|
41
|
+
//
|
|
42
|
+
// When F0.4's IPC primitive cannot be exercised inside an F0.5 sandbox profile,
|
|
43
|
+
// the profile MUST emit a structured carve-out log entry of the form:
|
|
44
|
+
//
|
|
45
|
+
// { level: 'warn', event: 'f04_sandbox_carveout',
|
|
46
|
+
// primitive: <'SO_PEERCRED'|'LOCAL_PEERCRED'|'GetNamedPipeClientProcessId'>,
|
|
47
|
+
// sandbox: <profile-name>, reason: <string> }
|
|
48
|
+
//
|
|
49
|
+
// The carve-out is ONLY for the specific IPC primitive, never a general loosening.
|
|
50
|
+
// `knosky doctor` reads this log and surfaces it to the operator.
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export const SANDBOX_CARVEOUT_EVENT = 'f04_sandbox_carveout';
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// SO_PEERCRED helper binary (Linux)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
//
|
|
58
|
+
// Node.js does not expose getsockopt(SO_PEERCRED) natively. We compile a tiny
|
|
59
|
+
// C helper on first use. The helper receives the socket fd on its own stdin (fd 0)
|
|
60
|
+
// and prints "pid uid gid\n" to stdout. Receiving via fd 0 lets Node pass the
|
|
61
|
+
// live fd without a subprocess fd-inheritance gap (the handle is passed directly
|
|
62
|
+
// in the stdio option array).
|
|
63
|
+
//
|
|
64
|
+
// The binary is placed under a process-scoped temp directory and reused for the
|
|
65
|
+
// process lifetime. On platforms where cc/gcc is unavailable the IPC identity
|
|
66
|
+
// layer degrades gracefully (returns null with a diagnostic).
|
|
67
|
+
|
|
68
|
+
const HELPER_SRC_LINUX = `
|
|
69
|
+
#define _GNU_SOURCE
|
|
70
|
+
#include <stdio.h>
|
|
71
|
+
#include <string.h>
|
|
72
|
+
#include <sys/socket.h>
|
|
73
|
+
#include <unistd.h>
|
|
74
|
+
|
|
75
|
+
/* Receive the socket fd on stdin (fd 0) via the Node.js stdio handle pass.
|
|
76
|
+
Call getsockopt SO_PEERCRED on that fd, print pid uid gid. */
|
|
77
|
+
int main(void) {
|
|
78
|
+
struct ucred cred;
|
|
79
|
+
memset(&cred, 0, sizeof(cred));
|
|
80
|
+
socklen_t len = sizeof(cred);
|
|
81
|
+
if (getsockopt(0, SOL_SOCKET, SO_PEERCRED, &cred, &len) < 0) {
|
|
82
|
+
perror("getsockopt SO_PEERCRED");
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
printf("%d %d %d\\n", (int)cred.pid, (int)cred.uid, (int)cred.gid);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
// macOS uses LOCAL_PEERCRED with struct xucred (different from Linux ucred).
|
|
91
|
+
const HELPER_SRC_MACOS = `
|
|
92
|
+
#include <stdio.h>
|
|
93
|
+
#include <string.h>
|
|
94
|
+
#include <sys/socket.h>
|
|
95
|
+
#include <sys/ucred.h>
|
|
96
|
+
#include <unistd.h>
|
|
97
|
+
|
|
98
|
+
int main(void) {
|
|
99
|
+
struct xucred cred;
|
|
100
|
+
memset(&cred, 0, sizeof(cred));
|
|
101
|
+
socklen_t len = sizeof(cred);
|
|
102
|
+
/* LOCAL_PEERCRED on macOS returns the peer's effective uid/gids.
|
|
103
|
+
Note: macOS does not include pid in xucred; pid is obtained via
|
|
104
|
+
LOCAL_PEEREPID (separate getsockopt call). */
|
|
105
|
+
if (getsockopt(0, SOL_LOCAL, LOCAL_PEERCRED, &cred, &len) < 0) {
|
|
106
|
+
perror("getsockopt LOCAL_PEERCRED");
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
int pid = 0;
|
|
110
|
+
socklen_t pidlen = sizeof(pid);
|
|
111
|
+
/* LOCAL_PEEREPID = 7 on macOS */
|
|
112
|
+
getsockopt(0, SOL_LOCAL, 7, &pid, &pidlen);
|
|
113
|
+
printf("%d %d 0\\n", pid, (int)cred.cr_uid);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
/** @type {string|null} cached path to the compiled helper binary */
|
|
119
|
+
let _helperPath = null;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compile and cache the peercred helper binary for the current platform.
|
|
123
|
+
* Returns the absolute path to the compiled binary, or null if compilation fails.
|
|
124
|
+
*
|
|
125
|
+
* @returns {string|null}
|
|
126
|
+
*/
|
|
127
|
+
function compilePeercredHelper() {
|
|
128
|
+
if (_helperPath !== null) return _helperPath;
|
|
129
|
+
|
|
130
|
+
const src = PLATFORM === 'linux' ? HELPER_SRC_LINUX
|
|
131
|
+
: PLATFORM === 'darwin' ? HELPER_SRC_MACOS
|
|
132
|
+
: null;
|
|
133
|
+
if (!src) return null; // Windows uses a different mechanism
|
|
134
|
+
|
|
135
|
+
let dir;
|
|
136
|
+
try {
|
|
137
|
+
dir = mkdtempSync(join(tmpdir(), 'knosky-ipc-'));
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const srcPath = join(dir, 'peercred.c');
|
|
143
|
+
const binPath = join(dir, 'peercred');
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
writeFileSync(srcPath, src, 'utf8');
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Try cc, then gcc, then clang in order.
|
|
152
|
+
const compilers = ['cc', 'gcc', 'clang'];
|
|
153
|
+
for (const cc of compilers) {
|
|
154
|
+
try {
|
|
155
|
+
execFileSync(cc, ['-O2', '-o', binPath, srcPath], { stdio: 'ignore', timeout: 15000 });
|
|
156
|
+
_helperPath = binPath;
|
|
157
|
+
return binPath;
|
|
158
|
+
} catch {
|
|
159
|
+
// try next compiler
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null; // no compiler available
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// getPeerIdentity — main public API
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @typedef {Object} PeerIdentity
|
|
172
|
+
* @property {number} pid - Kernel-reported PID of the peer process.
|
|
173
|
+
* @property {number} uid - Kernel-reported UID of the peer process.
|
|
174
|
+
* @property {number} gid - Kernel-reported GID of the peer process (0 on macOS).
|
|
175
|
+
* @property {string} mechanism - Which kernel primitive was used.
|
|
176
|
+
* @property {string} platform - OS platform string.
|
|
177
|
+
*/
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Obtain the kernel-verified identity of the process connected to `socketHandle`.
|
|
181
|
+
*
|
|
182
|
+
* `socketHandle` must be a live `net.Socket._handle` (a TCP/pipe handle object
|
|
183
|
+
* from Node.js's internal binding layer) that exposes an `fd` property. The
|
|
184
|
+
* function reads the socket's peer credentials directly from the OS kernel —
|
|
185
|
+
* the peer process cannot forge these values.
|
|
186
|
+
*
|
|
187
|
+
* Returns `null` (never throws) when:
|
|
188
|
+
* - the platform is not yet supported
|
|
189
|
+
* - the handle has no accessible fd
|
|
190
|
+
* - the compiler is unavailable (Linux/macOS)
|
|
191
|
+
* - the kernel call itself fails (e.g. inside an F0.5 sandbox)
|
|
192
|
+
*
|
|
193
|
+
* When returning `null` on Linux/macOS the caller SHOULD emit a
|
|
194
|
+
* SANDBOX_CARVEOUT_EVENT log entry identifying the specific primitive that
|
|
195
|
+
* failed; that entry surfaces in `knosky doctor`.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} socketHandle - net.Socket._handle from an accepted connection.
|
|
198
|
+
* @returns {PeerIdentity|null}
|
|
199
|
+
*/
|
|
200
|
+
export function getPeerIdentity(socketHandle) {
|
|
201
|
+
if (PLATFORM === 'linux' || PLATFORM === 'darwin') {
|
|
202
|
+
return _getPeerIdentityUnix(socketHandle);
|
|
203
|
+
}
|
|
204
|
+
if (PLATFORM === 'win32') {
|
|
205
|
+
return _getPeerIdentityWin32(socketHandle);
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Linux / macOS implementation: SO_PEERCRED / LOCAL_PEERCRED via helper binary.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} socketHandle
|
|
214
|
+
* @returns {PeerIdentity|null}
|
|
215
|
+
*/
|
|
216
|
+
function _getPeerIdentityUnix(socketHandle) {
|
|
217
|
+
const helperBin = compilePeercredHelper();
|
|
218
|
+
if (!helperBin) return null;
|
|
219
|
+
|
|
220
|
+
if (!socketHandle || typeof socketHandle !== 'object') return null;
|
|
221
|
+
|
|
222
|
+
// Pass the socket fd to the helper via its stdin slot (position 0 in stdio).
|
|
223
|
+
// Node's spawnSync supports passing a handle object directly in the stdio
|
|
224
|
+
// array; this causes libuv to dup the fd into the child's stdin.
|
|
225
|
+
let result;
|
|
226
|
+
try {
|
|
227
|
+
result = spawnSync(helperBin, [], {
|
|
228
|
+
stdio: [socketHandle, 'pipe', 'ignore'],
|
|
229
|
+
encoding: 'utf8',
|
|
230
|
+
timeout: 5000,
|
|
231
|
+
});
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (result.status !== 0 || result.signal !== null) return null;
|
|
237
|
+
|
|
238
|
+
const parts = (result.stdout || '').trim().split(' ').map(Number);
|
|
239
|
+
if (parts.length < 3 || parts.some(n => !Number.isInteger(n) || n < 0)) return null;
|
|
240
|
+
|
|
241
|
+
const [pid, uid, gid] = parts;
|
|
242
|
+
// pid === 0 is suspicious on Linux (only kernel threads have pid 0).
|
|
243
|
+
// Accept on macOS where LOCAL_PEEREPID may return 0 in some edge cases.
|
|
244
|
+
if (PLATFORM === 'linux' && pid === 0) return null;
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
pid,
|
|
248
|
+
uid,
|
|
249
|
+
gid,
|
|
250
|
+
mechanism: PLATFORM === 'linux' ? 'SO_PEERCRED' : 'LOCAL_PEERCRED',
|
|
251
|
+
platform: PLATFORM,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Windows implementation: GetNamedPipeClientProcessId + OpenProcessToken.
|
|
257
|
+
*
|
|
258
|
+
* On Windows, knosky uses a named pipe for local IPC. The kernel provides
|
|
259
|
+
* the client's PID via GetNamedPipeClientProcessId; the token (uid analogue)
|
|
260
|
+
* is obtained via ImpersonateNamedPipeClient + OpenProcessToken.
|
|
261
|
+
*
|
|
262
|
+
* This implementation calls a PowerShell one-liner with the pipe handle so
|
|
263
|
+
* that no C++ addon is required for the Windows path.
|
|
264
|
+
*
|
|
265
|
+
* @param {object} socketHandle
|
|
266
|
+
* @returns {PeerIdentity|null}
|
|
267
|
+
*/
|
|
268
|
+
function _getPeerIdentityWin32(socketHandle) {
|
|
269
|
+
// Windows named pipes expose the fd as a HANDLE value.
|
|
270
|
+
const fd = socketHandle?.fd;
|
|
271
|
+
if (typeof fd !== 'number' || fd < 0) return null;
|
|
272
|
+
|
|
273
|
+
// PowerShell script: call GetNamedPipeClientProcessId on the handle value,
|
|
274
|
+
// output JSON {pid, sid}. The SID encodes the uid analogue on Windows.
|
|
275
|
+
const ps = [
|
|
276
|
+
'$h=[IntPtr]' + fd + ';',
|
|
277
|
+
'$pid=0;',
|
|
278
|
+
'[void][KnoskySec.Pipe]::GetNamedPipeClientProcessId($h,[ref]$pid);',
|
|
279
|
+
'$tok=[System.IntPtr]::Zero;',
|
|
280
|
+
'[KnoskySec.Pipe]::OpenProcessToken([Diagnostics.Process]::GetProcessById($pid).Handle,8,[ref]$tok)|Out-Null;',
|
|
281
|
+
'$id=New-Object System.Security.Principal.WindowsIdentity $tok;',
|
|
282
|
+
'Write-Output ("{0} {1}" -f $pid,$id.User.Value)',
|
|
283
|
+
].join(' ');
|
|
284
|
+
|
|
285
|
+
let result;
|
|
286
|
+
try {
|
|
287
|
+
result = spawnSync('powershell.exe', ['-NoProfile', '-Command', ps], {
|
|
288
|
+
encoding: 'utf8',
|
|
289
|
+
timeout: 10000,
|
|
290
|
+
});
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (result.status !== 0) return null;
|
|
296
|
+
|
|
297
|
+
const line = (result.stdout || '').trim();
|
|
298
|
+
const m = line.match(/^(\d+)\s+(.+)$/);
|
|
299
|
+
if (!m) return null;
|
|
300
|
+
|
|
301
|
+
const pid = Number(m[1]);
|
|
302
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
pid,
|
|
306
|
+
uid: -1, // Windows uses SID, not numeric uid
|
|
307
|
+
gid: -1,
|
|
308
|
+
sid: m[2], // Windows Security Identifier
|
|
309
|
+
mechanism: 'GetNamedPipeClientProcessId',
|
|
310
|
+
platform: 'win32',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// resolveLeaseIdentity — swarm-lease anti-spoofing (GLM round-3 fix)
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @typedef {Object} LeaseRecord
|
|
320
|
+
* @property {string} leaseId - Stable, server-assigned lease identifier.
|
|
321
|
+
* @property {string} agentId - Authoritative agent identity (server-side record).
|
|
322
|
+
* @property {number} [pid] - PID of the process that registered the lease (optional).
|
|
323
|
+
* @property {number} [uid] - UID of the process that registered the lease (optional).
|
|
324
|
+
* @property {string} [status] - 'active' | 'expired' | 'revoked'.
|
|
325
|
+
*/
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Resolve the authoritative agent identity for an incoming request.
|
|
329
|
+
*
|
|
330
|
+
* CRITICAL invariant (GLM round-3): the evaluator NEVER trusts `payloadAgentId`
|
|
331
|
+
* (the `agentId` field from the request payload) as truth. Instead it looks up
|
|
332
|
+
* the lease record server-side using `leaseId`. The record's own `agentId` is
|
|
333
|
+
* returned as the authoritative identity.
|
|
334
|
+
*
|
|
335
|
+
* If `payloadAgentId` is supplied AND does not match the record's `agentId`,
|
|
336
|
+
* the call returns a spoofing-attempt error rather than accepting either value.
|
|
337
|
+
* Payload copy is checked-not-trusted: the check closes the agent-spoofing-
|
|
338
|
+
* under-shared-UID gap described in the ticket.
|
|
339
|
+
*
|
|
340
|
+
* @param {Map<string, LeaseRecord>} leaseStore
|
|
341
|
+
* Server-side, authoritative store of live lease records (keyed by leaseId).
|
|
342
|
+
* @param {string} leaseId
|
|
343
|
+
* The leaseId extracted from the incoming request (used only as a lookup key).
|
|
344
|
+
* @param {string|null} [payloadAgentId]
|
|
345
|
+
* The agentId the request self-asserts (optional; never used as truth).
|
|
346
|
+
* @returns {{ ok: true, agentId: string, lease: LeaseRecord }
|
|
347
|
+
* |{ ok: false, reason: string }}
|
|
348
|
+
*/
|
|
349
|
+
export function resolveLeaseIdentity(leaseStore, leaseId, payloadAgentId = null) {
|
|
350
|
+
if (!leaseId || typeof leaseId !== 'string') {
|
|
351
|
+
return { ok: false, reason: 'missing_lease_id' };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const lease = leaseStore instanceof Map ? leaseStore.get(leaseId) : null;
|
|
355
|
+
if (!lease) {
|
|
356
|
+
return { ok: false, reason: 'unknown_lease_id' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// TTL: reject active leases past expires_at even if status not swept yet
|
|
360
|
+
if (lease.expires_at) {
|
|
361
|
+
const exp = Date.parse(lease.expires_at);
|
|
362
|
+
if (Number.isFinite(exp) && exp <= Date.now()) {
|
|
363
|
+
return { ok: false, reason: 'lease_expired' };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (lease.status === 'expired') {
|
|
368
|
+
return { ok: false, reason: 'lease_expired' };
|
|
369
|
+
}
|
|
370
|
+
if (lease.status === 'revoked') {
|
|
371
|
+
return { ok: false, reason: 'lease_revoked' };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const authoritative = lease.agentId;
|
|
375
|
+
if (!authoritative || typeof authoritative !== 'string') {
|
|
376
|
+
return { ok: false, reason: 'lease_missing_agent_id' };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check-not-trust: if the request also asserts an agentId, verify it agrees
|
|
380
|
+
// with the server-side record. A mismatch signals a spoofing attempt.
|
|
381
|
+
if (payloadAgentId !== null && payloadAgentId !== undefined) {
|
|
382
|
+
if (payloadAgentId !== authoritative) {
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
reason: 'payload_agent_id_mismatch',
|
|
386
|
+
// Do not echo back either value — let the caller log as needed.
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { ok: true, agentId: authoritative, lease };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// conformanceReport — used by F0.4↔F0.5 sandbox conformance fixtures
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Run a self-check of the F0.4 identity mechanism and return a structured report.
|
|
400
|
+
*
|
|
401
|
+
* Called by each F0.5 sandbox conformance fixture to verify that F0.4 still
|
|
402
|
+
* functions correctly inside that specific sandbox before the profile is
|
|
403
|
+
* considered complete. If the check fails the fixture MUST emit a
|
|
404
|
+
* SANDBOX_CARVEOUT_EVENT log entry for the specific IPC primitive and surface
|
|
405
|
+
* it in `knosky doctor`.
|
|
406
|
+
*
|
|
407
|
+
* The report shape:
|
|
408
|
+
* ```
|
|
409
|
+
* {
|
|
410
|
+
* platform: string,
|
|
411
|
+
* mechanism: 'SO_PEERCRED'|'LOCAL_PEERCRED'|'GetNamedPipeClientProcessId'|'unsupported',
|
|
412
|
+
* compilerAvailable: boolean, // Linux / macOS
|
|
413
|
+
* selfTestPassed: boolean,
|
|
414
|
+
* selfTestPid: number|null, // must equal process.pid when selfTestPassed
|
|
415
|
+
* error: string|null,
|
|
416
|
+
* }
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* @returns {Promise<object>}
|
|
420
|
+
*/
|
|
421
|
+
export async function conformanceReport() {
|
|
422
|
+
if (PLATFORM === 'win32') {
|
|
423
|
+
// Windows: simple named-pipe self-test is complex to set up here;
|
|
424
|
+
// return a partial report. Full conformance is done by the F0.5 fixture.
|
|
425
|
+
return {
|
|
426
|
+
platform: 'win32',
|
|
427
|
+
mechanism: 'GetNamedPipeClientProcessId',
|
|
428
|
+
compilerAvailable: null,
|
|
429
|
+
selfTestPassed: false,
|
|
430
|
+
selfTestPid: null,
|
|
431
|
+
error: 'windows conformance test requires F0.5 fixture setup',
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const helperBin = compilePeercredHelper();
|
|
436
|
+
const mechanism = PLATFORM === 'linux' ? 'SO_PEERCRED' : 'LOCAL_PEERCRED';
|
|
437
|
+
|
|
438
|
+
if (!helperBin) {
|
|
439
|
+
return {
|
|
440
|
+
platform: PLATFORM,
|
|
441
|
+
mechanism,
|
|
442
|
+
compilerAvailable: false,
|
|
443
|
+
selfTestPassed: false,
|
|
444
|
+
selfTestPid: null,
|
|
445
|
+
error: 'no C compiler available (cc/gcc/clang); could not compile peercred helper',
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Run a loopback self-test: this process connects to itself over a Unix socket
|
|
450
|
+
// and reads back its own pid via getPeerIdentity().
|
|
451
|
+
const { createServer, connect } = await import('node:net');
|
|
452
|
+
const { join: pjoin } = await import('node:path');
|
|
453
|
+
const { mkdtempSync: mkdtmp, unlinkSync: unlink } = await import('node:fs');
|
|
454
|
+
|
|
455
|
+
const sockDir = mkdtmp(join(tmpdir(), 'knosky-f04-conf-'));
|
|
456
|
+
const sockPath = pjoin(sockDir, 'self.sock');
|
|
457
|
+
|
|
458
|
+
let selfTestPassed = false;
|
|
459
|
+
let selfTestPid = null;
|
|
460
|
+
let error = null;
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
await new Promise((resolve, reject) => {
|
|
464
|
+
const srv = createServer((sock) => {
|
|
465
|
+
const identity = getPeerIdentity(sock._handle);
|
|
466
|
+
if (identity && identity.pid === process.pid) {
|
|
467
|
+
selfTestPassed = true;
|
|
468
|
+
selfTestPid = identity.pid;
|
|
469
|
+
} else {
|
|
470
|
+
error = identity
|
|
471
|
+
? `pid mismatch: expected ${process.pid}, got ${identity.pid}`
|
|
472
|
+
: 'getPeerIdentity returned null';
|
|
473
|
+
}
|
|
474
|
+
sock.destroy();
|
|
475
|
+
srv.close(resolve);
|
|
476
|
+
});
|
|
477
|
+
srv.on('error', reject);
|
|
478
|
+
srv.listen(sockPath, () => {
|
|
479
|
+
const c = connect(sockPath);
|
|
480
|
+
c.on('error', reject);
|
|
481
|
+
c.once('connect', () => c.destroy());
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
} catch (e) {
|
|
485
|
+
error = String(e?.message || e);
|
|
486
|
+
selfTestPassed = false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try { unlink(sockPath); } catch { /* best effort */ }
|
|
490
|
+
try { unlink(sockDir); } catch { /* best effort */ }
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
platform: PLATFORM,
|
|
494
|
+
mechanism,
|
|
495
|
+
compilerAvailable: true,
|
|
496
|
+
selfTestPassed,
|
|
497
|
+
selfTestPid,
|
|
498
|
+
error,
|
|
499
|
+
};
|
|
500
|
+
}
|