litmus-cli 1.4.3 → 1.4.5
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/dist/commands/doctor.d.ts +2 -21
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +41 -33
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +6 -0
- package/dist/commands/submit.js.map +1 -1
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +33 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/detection.d.ts +32 -0
- package/dist/lib/detection.d.ts.map +1 -0
- package/dist/lib/detection.js +80 -0
- package/dist/lib/detection.js.map +1 -0
- package/dist/lib/hook-logger.cjs +50 -2
- package/dist/lib/platform.d.ts +34 -10
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/platform.js +56 -16
- package/dist/lib/platform.js.map +1 -1
- package/dist/lib/shadow.d.ts +79 -0
- package/dist/lib/shadow.d.ts.map +1 -0
- package/dist/lib/shadow.js +268 -0
- package/dist/lib/shadow.js.map +1 -0
- package/dist/lib/tracker.d.ts +1 -1
- package/dist/lib/tracker.d.ts.map +1 -1
- package/dist/lib/tracker.js +42 -4
- package/dist/lib/tracker.js.map +1 -1
- package/dist/lib/watcher.js +273 -25
- package/dist/lib/watcher.js.map +1 -1
- package/dist/lib/zip.d.ts.map +1 -1
- package/dist/lib/zip.js +24 -1
- package/dist/lib/zip.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude surface classification — pure functions, unit-testable (watcher.ts
|
|
3
|
+
* is a spawned script, so logic living there can't be imported by tests).
|
|
4
|
+
*
|
|
5
|
+
* "claude" on a candidate machine is at least three different products, and
|
|
6
|
+
* the old bare-word pattern labeled them all `claude` — which report readers
|
|
7
|
+
* took to mean Claude Code with zero prompts captured, i.e. broken tracking:
|
|
8
|
+
* claude_code — the CLI / IDE extension; prompts capturable via hook
|
|
9
|
+
* claude_desktop — the Desktop app; no hook exists
|
|
10
|
+
* claude_web — claude.ai in a browser tab; no hook can exist
|
|
11
|
+
* claude — legacy/unattributable, kept so old readers keep working
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Classifies every process-list line containing "claude" into surfaces.
|
|
15
|
+
* `ps` is the lowercased `ps aux` / `tasklist` output from getProcessList().
|
|
16
|
+
* A line matching no surface pattern stays legacy `claude` (unattributable —
|
|
17
|
+
* notably Windows tasklist, where Desktop and Claude Code are both bare
|
|
18
|
+
* `claude.exe` image names, and bystander strings like the VS Code
|
|
19
|
+
* extension's install path in some process's args).
|
|
20
|
+
*/
|
|
21
|
+
export function classifyClaudeProcesses(ps) {
|
|
22
|
+
const found = new Set();
|
|
23
|
+
for (const line of ps.split("\n")) {
|
|
24
|
+
if (!/\bclaude\b/.test(line))
|
|
25
|
+
continue;
|
|
26
|
+
if (/claude\.app|claude helper/.test(line)) {
|
|
27
|
+
// macOS Desktop app bundle and its renderer/GPU helpers
|
|
28
|
+
found.add("claude_desktop");
|
|
29
|
+
}
|
|
30
|
+
else if (/claude-code[/\\]cli\.js|[/\\]claude(?:\s|$)/.test(line)) {
|
|
31
|
+
// The npm entrypoint, or the `claude` binary itself as the executed
|
|
32
|
+
// path. Path-anchored so the word inside a directory name (the VS Code
|
|
33
|
+
// extension's install path, a .claude config dir) doesn't count as the
|
|
34
|
+
// CLI running. Desktop's binary also ends in /claude but its lines
|
|
35
|
+
// match the app-bundle branch above first.
|
|
36
|
+
found.add("claude_code");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
found.add("claude");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...found];
|
|
43
|
+
}
|
|
44
|
+
// Owners that mean "a browser tab". First-token matching: lsof reports
|
|
45
|
+
// multi-word app names ("Google Chrome Helper", "Brave Browser") but the
|
|
46
|
+
// leading token identifies the browser; Safari's network traffic comes from
|
|
47
|
+
// com.apple.WebKit.Networking.
|
|
48
|
+
const BROWSER_OWNER_RE = /^(google|chrome|chromium|safari|com\.apple\.webkit|firefox|msedge|microsoft|edge|brave|arc|opera|vivaldi)/i;
|
|
49
|
+
/**
|
|
50
|
+
* Re-labels an Anthropic-bound connection by its owning process. The IP alone
|
|
51
|
+
* cannot separate the surfaces (claude.ai is Cloudflare-fronted and shares
|
|
52
|
+
* IPs), but the owner can, and each surface reads very differently in a
|
|
53
|
+
* report: Claude Code captures prompts via hook; Claude Desktop and a browser
|
|
54
|
+
* tab cannot capture anything.
|
|
55
|
+
*
|
|
56
|
+
* `owner` is the process name exactly as the OS reported it — case matters.
|
|
57
|
+
*/
|
|
58
|
+
export function refineClaudeTool(tool, owner) {
|
|
59
|
+
if (tool !== "claude_web" && tool !== "claude_api")
|
|
60
|
+
return tool;
|
|
61
|
+
if (!owner)
|
|
62
|
+
return tool; // OS gave no owner — keep the IP-derived label
|
|
63
|
+
// Windows image name: Desktop and Claude Code are both `claude.exe`, so the
|
|
64
|
+
// surface stays unattributable.
|
|
65
|
+
if (/^claude\.exe$/i.test(owner))
|
|
66
|
+
return "claude";
|
|
67
|
+
// Case is the signal on mac/linux: `claude` is the Claude Code CLI binary,
|
|
68
|
+
// `Claude` / `Claude Helper …` is the Desktop app (verified live on macOS).
|
|
69
|
+
if (/^claude(-code)?$/.test(owner))
|
|
70
|
+
return "claude_code";
|
|
71
|
+
if (/^Claude\b/.test(owner))
|
|
72
|
+
return "claude_desktop";
|
|
73
|
+
if (BROWSER_OWNER_RE.test(owner))
|
|
74
|
+
return tool;
|
|
75
|
+
// Some other app owns the socket (an Electron webview, a shared Cloudflare
|
|
76
|
+
// IP that isn't Anthropic at all). claude.ai traffic keeps the vendor but
|
|
77
|
+
// drops the surface claim; api traffic keeps its API label.
|
|
78
|
+
return tool === "claude_web" ? "claude" : tool;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/lib/detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAU;IAChD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAQ;QACtC,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,wDAAwD;YACxD,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QAC7B,CAAC;aAAM,IAAI,6CAA6C,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpE,oEAAoE;YACpE,uEAAuE;YACvE,uEAAuE;YACvE,mEAAmE;YACnE,2CAA2C;YAC3C,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAA;AACnB,CAAC;AAED,uEAAuE;AACvE,yEAAyE;AACzE,4EAA4E;AAC5E,+BAA+B;AAC/B,MAAM,gBAAgB,GAAG,4GAA4G,CAAA;AAErI;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAA;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA,CAAC,+CAA+C;IACvE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IACjD,2EAA2E;IAC3E,4EAA4E;IAC5E,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAA;IACxD,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAA;IACpD,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;AAChD,CAAC"}
|
package/dist/lib/hook-logger.cjs
CHANGED
|
@@ -155,6 +155,47 @@ function isAssessmentLitmusDir(litmusDir) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// ── Shadow fallback (ENG-1453; kept in step with cli/src/lib/shadow.ts) ──
|
|
159
|
+
// `litmus init`/`doctor` mirror each assessment's config to
|
|
160
|
+
// ~/.litmus/assessments/<assessmentId>/config.json, stamped with the project
|
|
161
|
+
// dir it was written for. When the in-repo config is deleted mid-session
|
|
162
|
+
// (the ENG-1446 incident), that mirror lets capture keep working instead of
|
|
163
|
+
// exiting silently. Gated on the caller: only registry-listed project dirs
|
|
164
|
+
// that still exist are ever matched, so a deleted or retired assessment can
|
|
165
|
+
// never resurrect capture (ENG-953 principle).
|
|
166
|
+
|
|
167
|
+
function realpathOr(p) {
|
|
168
|
+
// macOS: process.cwd() is realpath-resolved but stored paths may not be
|
|
169
|
+
// (e.g. /var vs /private/var), so compare both sides through realpath.
|
|
170
|
+
try { return fs.realpathSync(p) } catch { return path.resolve(p) }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readShadowConfigForProject(projectDir) {
|
|
174
|
+
try {
|
|
175
|
+
const home = os.homedir()
|
|
176
|
+
if (!home) return null
|
|
177
|
+
const root = path.join(home, ".litmus", "assessments")
|
|
178
|
+
const target = realpathOr(projectDir)
|
|
179
|
+
for (const id of fs.readdirSync(root)) {
|
|
180
|
+
let cfg
|
|
181
|
+
try {
|
|
182
|
+
cfg = JSON.parse(fs.readFileSync(path.join(root, id, "config.json"), "utf8"))
|
|
183
|
+
} catch { continue }
|
|
184
|
+
if (!cfg || typeof cfg.projectDir !== "string" || typeof cfg.token !== "string") continue
|
|
185
|
+
if (realpathOr(cfg.projectDir) === target) return cfg
|
|
186
|
+
}
|
|
187
|
+
} catch { /* no shadows on this machine */ }
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isLiveProjectDir(p) {
|
|
192
|
+
try {
|
|
193
|
+
return fs.statSync(p).isDirectory()
|
|
194
|
+
} catch {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
158
199
|
// Walk up from `startDir` looking for an assessment .litmus dir.
|
|
159
200
|
function walkUpForLitmusDir(startDir) {
|
|
160
201
|
let dir = startDir
|
|
@@ -222,7 +263,12 @@ function findLitmusDir(hintDirs = [], sessionDirs = []) {
|
|
|
222
263
|
const list = JSON.parse(fs.readFileSync(registry, "utf8"))
|
|
223
264
|
if (!Array.isArray(list)) return null
|
|
224
265
|
const valid = list.filter(
|
|
225
|
-
p => typeof p === "string" &&
|
|
266
|
+
p => typeof p === "string" && (
|
|
267
|
+
isAssessmentLitmusDir(path.join(p, ".litmus"))
|
|
268
|
+
// ENG-1453: config wiped but the assessment is alive — the dir still
|
|
269
|
+
// exists and its shadow mirror supplies identity + credentials.
|
|
270
|
+
|| (isLiveProjectDir(p) && readShadowConfigForProject(p) !== null)
|
|
271
|
+
),
|
|
226
272
|
)
|
|
227
273
|
if (!valid.length) return null
|
|
228
274
|
|
|
@@ -245,7 +291,9 @@ function readConfig(litmusDir) {
|
|
|
245
291
|
const configPath = path.join(litmusDir, "config.json")
|
|
246
292
|
return JSON.parse(fs.readFileSync(configPath, "utf8"))
|
|
247
293
|
} catch {
|
|
248
|
-
|
|
294
|
+
// ENG-1453: in-repo config gone — the shadow mirror keeps capture alive
|
|
295
|
+
// until the watcher's self-heal restores the file.
|
|
296
|
+
return readShadowConfigForProject(path.dirname(litmusDir))
|
|
249
297
|
}
|
|
250
298
|
}
|
|
251
299
|
|
package/dist/lib/platform.d.ts
CHANGED
|
@@ -17,21 +17,45 @@ export declare function getProcessList(): string;
|
|
|
17
17
|
* Calls cb(err, stdout) with raw connection output.
|
|
18
18
|
* Handles platform fallbacks internally (lsof → ss on Linux).
|
|
19
19
|
*
|
|
20
|
-
* - macOS: `lsof -i -n -P`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* - macOS: `lsof -i -n -P +c0` (+c0 = untruncated COMMAND, so the owning
|
|
21
|
+
* process is attributable — default lsof cuts it at 9 chars, which mangles
|
|
22
|
+
* names like `com.apple.WebKit.Networking`)
|
|
23
|
+
* - Linux: same lsof, falls back to `ss -tnp` if lsof is unavailable
|
|
24
|
+
* - Windows: `netstat -ano` (PID only; resolve names via resolvePidNames)
|
|
23
25
|
*/
|
|
24
26
|
export declare function getConnections(cb: (err: Error | null, stdout?: string) => void): void;
|
|
27
|
+
export interface ConnectionMatch {
|
|
28
|
+
ip: string;
|
|
29
|
+
port: string;
|
|
30
|
+
/**
|
|
31
|
+
* Name of the process that owns the connection, exactly as the OS reported
|
|
32
|
+
* it — case matters (macOS: `Claude` is the Desktop app, `claude` is the
|
|
33
|
+
* Claude Code CLI). Empty when the OS output doesn't carry a name on the
|
|
34
|
+
* line (Windows netstat, permission-restricted ss rows).
|
|
35
|
+
*/
|
|
36
|
+
owner: string;
|
|
37
|
+
/** Owning PID, only where the OS gives a PID instead of a name (Windows). */
|
|
38
|
+
pid?: string;
|
|
39
|
+
}
|
|
25
40
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
41
|
+
* Parses a single connection output line into destination IP/port plus the
|
|
42
|
+
* owning process, or null if the line isn't a connection row.
|
|
28
43
|
*
|
|
29
44
|
* Format varies by tool:
|
|
30
|
-
* lsof: "... ->104.18.7.42:443
|
|
31
|
-
* ss:
|
|
32
|
-
* netstat: " TCP local:port foreign:port STATE PID"
|
|
45
|
+
* lsof: "COMMAND PID USER ... ->104.18.7.42:443 (ESTABLISHED)" (owner = col 1)
|
|
46
|
+
* ss -tnp: "... dst:port users:((\"chrome\",pid=123,fd=9))" (owner in users:())
|
|
47
|
+
* netstat: " TCP local:port foreign:port STATE PID" (PID only, last col)
|
|
33
48
|
*
|
|
34
|
-
* On Unix, trying both lsof and ss patterns is safe — they don't cross-match
|
|
49
|
+
* On Unix, trying both lsof and ss patterns is safe — they don't cross-match:
|
|
50
|
+
* lsof peers carry "->", and an ss peer is followed only by the optional
|
|
51
|
+
* users:() column.
|
|
35
52
|
*/
|
|
36
|
-
export declare function
|
|
53
|
+
export declare function extractConnection(line: string): ConnectionMatch | null;
|
|
54
|
+
/**
|
|
55
|
+
* Resolves PIDs to process image names where connection output only carries
|
|
56
|
+
* PIDs (Windows netstat). Calls cb with a PID → name map, or null on
|
|
57
|
+
* platforms where connection lines already name their owner (Unix) or when
|
|
58
|
+
* tasklist fails. Never throws.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolvePidNames(cb: (names: Map<string, string> | null) => void): void;
|
|
37
61
|
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC;AAED
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAarF;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAqBtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAkBrF"}
|
package/dist/lib/platform.js
CHANGED
|
@@ -23,16 +23,18 @@ export function getProcessList() {
|
|
|
23
23
|
* Calls cb(err, stdout) with raw connection output.
|
|
24
24
|
* Handles platform fallbacks internally (lsof → ss on Linux).
|
|
25
25
|
*
|
|
26
|
-
* - macOS: `lsof -i -n -P`
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* - macOS: `lsof -i -n -P +c0` (+c0 = untruncated COMMAND, so the owning
|
|
27
|
+
* process is attributable — default lsof cuts it at 9 chars, which mangles
|
|
28
|
+
* names like `com.apple.WebKit.Networking`)
|
|
29
|
+
* - Linux: same lsof, falls back to `ss -tnp` if lsof is unavailable
|
|
30
|
+
* - Windows: `netstat -ano` (PID only; resolve names via resolvePidNames)
|
|
29
31
|
*/
|
|
30
32
|
export function getConnections(cb) {
|
|
31
33
|
if (isWin) {
|
|
32
34
|
execFile("netstat", ["-ano"], { timeout: 10000, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 }, cb);
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
35
|
-
execFile("lsof", ["-i", "-n", "-P"], { timeout: 10000, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
|
|
37
|
+
execFile("lsof", ["-i", "-n", "-P", "+c0"], { timeout: 10000, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
|
|
36
38
|
if (err && err.code === "ENOENT" && isLinux) {
|
|
37
39
|
// lsof unavailable on Linux — fall back to ss
|
|
38
40
|
execFile("ss", ["-tnp"], { timeout: 5000, encoding: "utf8", maxBuffer: 1024 * 1024 }, cb);
|
|
@@ -43,27 +45,65 @@ export function getConnections(cb) {
|
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
+
* Parses a single connection output line into destination IP/port plus the
|
|
49
|
+
* owning process, or null if the line isn't a connection row.
|
|
48
50
|
*
|
|
49
51
|
* Format varies by tool:
|
|
50
|
-
* lsof: "... ->104.18.7.42:443
|
|
51
|
-
* ss:
|
|
52
|
-
* netstat: " TCP local:port foreign:port STATE PID"
|
|
52
|
+
* lsof: "COMMAND PID USER ... ->104.18.7.42:443 (ESTABLISHED)" (owner = col 1)
|
|
53
|
+
* ss -tnp: "... dst:port users:((\"chrome\",pid=123,fd=9))" (owner in users:())
|
|
54
|
+
* netstat: " TCP local:port foreign:port STATE PID" (PID only, last col)
|
|
53
55
|
*
|
|
54
|
-
* On Unix, trying both lsof and ss patterns is safe — they don't cross-match
|
|
56
|
+
* On Unix, trying both lsof and ss patterns is safe — they don't cross-match:
|
|
57
|
+
* lsof peers carry "->", and an ss peer is followed only by the optional
|
|
58
|
+
* users:() column.
|
|
55
59
|
*/
|
|
56
|
-
export function
|
|
60
|
+
export function extractConnection(line) {
|
|
57
61
|
if (isWin) {
|
|
58
62
|
const parts = line.trim().split(/\s+/);
|
|
59
63
|
if (parts.length >= 4 && (parts[0] === "TCP" || parts[0] === "UDP")) {
|
|
60
|
-
|
|
64
|
+
const m = parts[2].match(/^(\d+\.\d+\.\d+\.\d+):(\d+)$/);
|
|
65
|
+
if (!m)
|
|
66
|
+
return null;
|
|
67
|
+
return { ip: m[1], port: m[2], owner: "", pid: parts[parts.length - 1] };
|
|
61
68
|
}
|
|
62
69
|
return null;
|
|
63
70
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
const lsof = line.match(/->(\d+\.\d+\.\d+\.\d+):(\d+)/);
|
|
72
|
+
if (lsof) {
|
|
73
|
+
return { ip: lsof[1], port: lsof[2], owner: line.split(/\s+/, 1)[0] ?? "" };
|
|
74
|
+
}
|
|
75
|
+
// ss: the peer address is followed by the users:() column when ss can see
|
|
76
|
+
// the owner (own processes, or root), and ends the line when it can't.
|
|
77
|
+
const ss = line.match(/\s(\d+\.\d+\.\d+\.\d+):(\d+)(?:\s+users:\(\("([^"]+)".*)?$/);
|
|
78
|
+
if (ss) {
|
|
79
|
+
return { ip: ss[1], port: ss[2], owner: ss[3] ?? "" };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolves PIDs to process image names where connection output only carries
|
|
85
|
+
* PIDs (Windows netstat). Calls cb with a PID → name map, or null on
|
|
86
|
+
* platforms where connection lines already name their owner (Unix) or when
|
|
87
|
+
* tasklist fails. Never throws.
|
|
88
|
+
*/
|
|
89
|
+
export function resolvePidNames(cb) {
|
|
90
|
+
if (!isWin) {
|
|
91
|
+
cb(null);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
execFile("tasklist", ["/fo", "csv", "/nh"], { timeout: 10000, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
|
|
95
|
+
if (err || !stdout) {
|
|
96
|
+
cb(null);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const names = new Map();
|
|
100
|
+
for (const line of stdout.split("\n")) {
|
|
101
|
+
// CSV row: "Image Name","PID","Session Name","Session#","Mem Usage"
|
|
102
|
+
const m = line.match(/^"([^"]+)","(\d+)"/);
|
|
103
|
+
if (m)
|
|
104
|
+
names.set(m[2], m[1]);
|
|
105
|
+
}
|
|
106
|
+
cb(names);
|
|
107
|
+
});
|
|
68
108
|
}
|
|
69
109
|
//# sourceMappingURL=platform.js.map
|
package/dist/lib/platform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAElD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAA;AAC1C,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAA;AAE5C;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAA;IACzC,OAAO,QAAQ,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;AACzE,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { LitmusConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shadow store for tracking-critical state (ENG-1453).
|
|
4
|
+
*
|
|
5
|
+
* Everything AI-capture depends on used to live only inside the assessment
|
|
6
|
+
* repo, gitignored — which put it squarely inside `git clean -fdX`'s blast
|
|
7
|
+
* radius (the ENG-1446 incident: one ordinary cleanup command deleted the
|
|
8
|
+
* config, the Copilot hook, and the forensic log at once). This module keeps
|
|
9
|
+
* a copy of the small, load-bearing pieces OUTSIDE the repo, next to the
|
|
10
|
+
* registry that already lives there:
|
|
11
|
+
*
|
|
12
|
+
* ~/.litmus/assessments/<assessmentId>/
|
|
13
|
+
* config.json verbatim mirror of .litmus/config.json + { projectDir, mirroredAt }
|
|
14
|
+
* chain.json last uploaded chain position { seq, hash }
|
|
15
|
+
* tracker.pid mirror — lets doctor find a live watcher after a repo wipe
|
|
16
|
+
* tracker.nonce mirror — same
|
|
17
|
+
* ~/.litmus/logs/<assessmentId>.log tracker log (relocated, not mirrored)
|
|
18
|
+
*
|
|
19
|
+
* Rules (from the ENG-1453 security review):
|
|
20
|
+
* - The in-repo config is ALWAYS authoritative when present; shadows are read
|
|
21
|
+
* only when it is missing.
|
|
22
|
+
* - Every write here is best-effort: a failing shadow write must never break
|
|
23
|
+
* init, doctor, or the watcher.
|
|
24
|
+
* - Shadows die with the assessment: deleted on submit/terminal outcomes and
|
|
25
|
+
* pruned when their project dir is gone and unregistered.
|
|
26
|
+
*/
|
|
27
|
+
export interface ShadowConfig extends LitmusConfig {
|
|
28
|
+
/** Absolute assessment root the mirror was written for — the lookup key
|
|
29
|
+
* when the in-repo config (and its directory) no longer exists. */
|
|
30
|
+
projectDir: string;
|
|
31
|
+
mirroredAt: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ChainCursor {
|
|
34
|
+
seq: number;
|
|
35
|
+
hash: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function shadowRootDir(): string;
|
|
39
|
+
export declare function shadowDirFor(assessmentId: string): string | null;
|
|
40
|
+
export declare function shadowLogPath(assessmentId: string): string | null;
|
|
41
|
+
/** Mirror a just-written config. Best-effort; returns whether it stuck. */
|
|
42
|
+
export declare function writeShadowConfig(projectDir: string, config: LitmusConfig): boolean;
|
|
43
|
+
export declare function readShadowConfigById(assessmentId: string): ShadowConfig | null;
|
|
44
|
+
/** Find the shadow whose mirror was written for this project dir. */
|
|
45
|
+
export declare function findShadowConfigForDir(projectDir: string): ShadowConfig | null;
|
|
46
|
+
/** Record the last chain position the SERVER has (written after each
|
|
47
|
+
* successful upload), so a replacement watcher whose activity.jsonl is gone
|
|
48
|
+
* continues the chain instead of forking a second genesis. */
|
|
49
|
+
export declare function writeShadowChainCursor(assessmentId: string, seq: number, hash: string): void;
|
|
50
|
+
export declare function readShadowChainCursor(assessmentId: string): ChainCursor | null;
|
|
51
|
+
export declare function writeShadowTrackerFile(assessmentId: string, name: "tracker.pid" | "tracker.nonce", content: string): void;
|
|
52
|
+
export declare function readShadowTrackerFile(assessmentId: string, name: "tracker.pid" | "tracker.nonce"): string | null;
|
|
53
|
+
export declare function shadowTrackerFileMtime(assessmentId: string, name: "tracker.pid" | "tracker.nonce"): number | null;
|
|
54
|
+
export declare function deleteShadowForAssessment(assessmentId: string): void;
|
|
55
|
+
/** Delete the shadow keyed to a project dir — works when the in-repo config
|
|
56
|
+
* (which would have named the assessment) is already gone. */
|
|
57
|
+
export declare function deleteShadowForProject(projectDir: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Remove shadows whose assessment is demonstrably over on this machine:
|
|
60
|
+
* project dir no longer exists AND no registry entry points at it. A live
|
|
61
|
+
* assessment whose folder was merely wiped keeps its registry entry, so it
|
|
62
|
+
* survives pruning until submit/expiry releases it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function pruneShadows(registryPaths: string[]): number;
|
|
65
|
+
export declare const REPAIR_FILE_NAME = "LITMUS-REPAIR.md";
|
|
66
|
+
/** Written by the watcher only when self-heal could not act (server
|
|
67
|
+
* unreachable, no usable shadow, AND the in-memory restore failed to write).
|
|
68
|
+
* Rewritten every heartbeat while the outage lasts — a cleanup pass that
|
|
69
|
+
* deletes it gets it back — and removed the moment the config is restored.
|
|
70
|
+
*
|
|
71
|
+
* Deliberately carries NO credentials: a project-root file can be committed,
|
|
72
|
+
* pushed, read into an AI agent's context, or shipped in the submission zip,
|
|
73
|
+
* so the token must never appear here. The candidate's assessment page
|
|
74
|
+
* ("Something not working?") shows the full copy-paste repair command, and
|
|
75
|
+
* `litmus doctor` run bare prompts for the token pointing at that panel. */
|
|
76
|
+
export declare function repairFileContent(): string;
|
|
77
|
+
export declare function writeRepairFile(projectDir: string): void;
|
|
78
|
+
export declare function removeRepairFile(projectDir: string): void;
|
|
79
|
+
//# sourceMappingURL=shadow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/lib/shadow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD;wEACoE;IACpE,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;CAClB;AAMD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAOD,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGjE;AA6BD,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAcnF;AAiBD,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAQ9E;AAED,qEAAqE;AACrE,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAO9E;AAcD;;+DAE+D;AAC/D,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS5F;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAU9E;AAID,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAOzH;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,GAAG,MAAM,GAAG,IAAI,CAQhH;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,GAAG,MAAM,GAAG,IAAI,CAQjH;AAID,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAQpE;AAED;+DAC+D;AAC/D,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAG/D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAe5D;AAID,eAAO,MAAM,gBAAgB,qBAAqB,CAAA;AAElD;;;;;;;;;6EAS6E;AAC7E,wBAAgB,iBAAiB,IAAI,MAAM,CAoB1C;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAEzD"}
|