litmus-cli 1.4.28 → 1.4.30

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 (39) hide show
  1. package/dist/commands/connect.d.ts +360 -14
  2. package/dist/commands/connect.d.ts.map +1 -1
  3. package/dist/commands/connect.js +767 -70
  4. package/dist/commands/connect.js.map +1 -1
  5. package/dist/commands/doctor.d.ts +52 -0
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +76 -7
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +6 -2
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/pause.d.ts +13 -8
  13. package/dist/commands/pause.d.ts.map +1 -1
  14. package/dist/commands/pause.js +18 -10
  15. package/dist/commands/pause.js.map +1 -1
  16. package/dist/commands/push.d.ts.map +1 -1
  17. package/dist/commands/push.js +6 -2
  18. package/dist/commands/push.js.map +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/lib/api-base.d.ts +80 -0
  22. package/dist/lib/api-base.d.ts.map +1 -1
  23. package/dist/lib/api-base.js +83 -0
  24. package/dist/lib/api-base.js.map +1 -1
  25. package/dist/lib/editor-binary.d.ts +110 -0
  26. package/dist/lib/editor-binary.d.ts.map +1 -0
  27. package/dist/lib/editor-binary.js +72 -0
  28. package/dist/lib/editor-binary.js.map +1 -0
  29. package/dist/lib/editor-ide.d.ts +11 -0
  30. package/dist/lib/editor-ide.d.ts.map +1 -0
  31. package/dist/lib/editor-ide.js +2 -0
  32. package/dist/lib/editor-ide.js.map +1 -0
  33. package/dist/lib/editor-server.d.ts +253 -0
  34. package/dist/lib/editor-server.d.ts.map +1 -0
  35. package/dist/lib/editor-server.js +286 -0
  36. package/dist/lib/editor-server.js.map +1 -0
  37. package/dist/lib/watcher.js +18 -1
  38. package/dist/lib/watcher.js.map +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,72 @@
1
+ import path from "path";
2
+ /**
3
+ * Where each editor's CLI lives when its installer put it there, per platform.
4
+ *
5
+ * Paths are built with the TARGET's separator (`path.win32` / `path.posix`) and never
6
+ * with the bare `path`, which is bound to the host: a Windows expectation assembled with
7
+ * a POSIX `path` passes on a POSIX host precisely when the shipped code is wrong there.
8
+ * That is `chat-model-config.ts`'s `pathFor` rule, and the tests beside this assert
9
+ * literal strings for the same reason.
10
+ */
11
+ export function editorInstallCandidates(ide, platform, env, home) {
12
+ if (platform === "darwin") {
13
+ const bundle = ide === "cursor" ? "Cursor.app" : "Visual Studio Code.app";
14
+ const bin = ide === "cursor" ? "cursor" : "code";
15
+ // A user-local install under ~/Applications is what a drag-install without admin
16
+ // rights produces, and is invisible to anything that only looks at /Applications.
17
+ return ["/Applications", path.posix.join(home, "Applications")].map((root) => path.posix.join(root, bundle, "Contents", "Resources", "app", "bin", bin));
18
+ }
19
+ if (platform === "win32") {
20
+ const local = env.LOCALAPPDATA;
21
+ const programFiles = env.ProgramFiles;
22
+ const programFilesX86 = env["ProgramFiles(x86)"];
23
+ const out = [];
24
+ if (ide === "cursor") {
25
+ // Cursor's Windows installer is per-user by default; the machine-wide option
26
+ // lands under Program Files with the same tree beneath it.
27
+ if (local)
28
+ out.push(path.win32.join(local, "Programs", "cursor", "resources", "app", "bin", "cursor.cmd"));
29
+ if (programFiles)
30
+ out.push(path.win32.join(programFiles, "cursor", "resources", "app", "bin", "cursor.cmd"));
31
+ }
32
+ else {
33
+ if (local)
34
+ out.push(path.win32.join(local, "Programs", "Microsoft VS Code", "bin", "code.cmd"));
35
+ if (programFiles)
36
+ out.push(path.win32.join(programFiles, "Microsoft VS Code", "bin", "code.cmd"));
37
+ if (programFilesX86)
38
+ out.push(path.win32.join(programFilesX86, "Microsoft VS Code", "bin", "code.cmd"));
39
+ }
40
+ return out;
41
+ }
42
+ // Linux. Package installs and the vendor tarballs; an AppImage left in ~/Downloads is
43
+ // not reachable from here and is the documented residual.
44
+ const bin = ide === "cursor" ? "cursor" : "code";
45
+ const roots = ide === "cursor"
46
+ ? ["/usr/share/cursor/bin", "/opt/Cursor/bin", "/opt/cursor/bin", path.posix.join(home, ".local", "bin")]
47
+ : ["/usr/share/code/bin", "/opt/visual-studio-code/bin", "/snap/bin", path.posix.join(home, ".local", "bin")];
48
+ return roots.map((root) => path.posix.join(root, bin));
49
+ }
50
+ const SHELL_SAFE_ARG = /^[A-Za-z0-9_@+=:,./\\-]+$/;
51
+ /**
52
+ * What a quoted `cmd.exe` token cannot carry. A `"` closes the quoting this opens, `%`
53
+ * is expanded inside quotes, and a control character can end the line outright.
54
+ * Everything else — spaces, `&`, `|`, `^`, parentheses — is literal once quoted, which
55
+ * is why the command may hold them and an unquoted ARGUMENT may not.
56
+ */
57
+ // eslint-disable-next-line no-control-regex
58
+ const WIN32_UNQUOTABLE_COMMAND = /["%\u0000-\u001f]/;
59
+ export function editorSpawnSpec(command, args, platform) {
60
+ if (platform !== "win32")
61
+ return { command, args, shell: false };
62
+ for (const a of args) {
63
+ if (!SHELL_SAFE_ARG.test(a))
64
+ throw new Error(`refusing to build a shell command line around: ${a}`);
65
+ }
66
+ // Not an error — see the block comment. A command we cannot quote is a command we
67
+ // cannot drive, which is the same answer as a command that will not run.
68
+ if (WIN32_UNQUOTABLE_COMMAND.test(command))
69
+ return null;
70
+ return { command: `"${command}"`, args, shell: true };
71
+ }
72
+ //# sourceMappingURL=editor-binary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor-binary.js","sourceRoot":"","sources":["../../src/lib/editor-binary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The native editors `litmus connect` can drive.
3
+ *
4
+ * It lives here rather than in `commands/connect.ts` only so that the lib modules the
5
+ * connect flow leans on — `editor-binary.ts`, and anything after it — can name an editor
6
+ * without importing the command back and closing a cycle. `connect.ts` re-exports it, so
7
+ * the record it belongs to (`NATIVE_EDITORS`, which carries every measured difference
8
+ * between the two) is still the one place that describes them.
9
+ */
10
+ export type NativeIde = "vscode" | "cursor";
11
+ //# sourceMappingURL=editor-ide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor-ide.d.ts","sourceRoot":"","sources":["../../src/lib/editor-ide.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=editor-ide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor-ide.js","sourceRoot":"","sources":["../../src/lib/editor-ide.ts"],"names":[],"mappings":""}
@@ -0,0 +1,253 @@
1
+ /**
2
+ * ENG-2332 — whether a native editor can, and then DID, start its server inside the
3
+ * workspace. Everything here is pure or takes its one side effect as an argument, so the
4
+ * decisions are testable without a Cursor install and without a live container.
5
+ *
6
+ * ## The failure this exists to stop
7
+ *
8
+ * A VS Code-family editor opening a Remote-SSH window does not run anything of ours: it
9
+ * copies its OWN server build into the remote home and launches it there. Which build is
10
+ * decided by the CLIENT, by commit, and the two editors are pinned to each other — a
11
+ * 3.19.13 client will not talk to a 3.20.21 server, it fetches its own.
12
+ *
13
+ * A Codespaces v2 container has sealed egress. It cannot reach `api2.cursor.sh`,
14
+ * `downloads.cursor.com` or `github.com` (measured from inside a prod workspace,
15
+ * 2026-09-15: `curl` returns 000 after a 12s timeout). The only route a non-baked client
16
+ * has is the mirror (`infra/vscode-mirror/`, ENG-1062 for VS Code, ENG-1265 + ENG-1936
17
+ * for Cursor), and a mirror route that has stopped matching what a client asks for is
18
+ * indistinguishable, from the client's side, from no route at all.
19
+ *
20
+ * What the client does then is the whole problem: it gives up and opens the window
21
+ * anyway. MEASURED on macOS against a live prod workspace (ENG-2332):
22
+ *
23
+ * - `cursor --remote ssh-remote+<alias> <dir>` exits 0 and prints nothing.
24
+ * - A window appears, carrying the remote authority — `storage.json` records
25
+ * `"remoteAuthority": "ssh-remote+<alias>"` — and Cursor's ordinary agent home
26
+ * screen. It is indistinguishable from a working one.
27
+ * - The renderer log says `Started local extension host`, and every
28
+ * `$updateShellExecCapabilities` is refused as `non-authoritative host`.
29
+ * - Nothing under `~/.cursor-server` on the VM is touched. No `data/logs` is created.
30
+ * No server process exists.
31
+ *
32
+ * So the candidate works in what looks like their editor, on their own laptop's files,
33
+ * and NOTHING IS RECORDED. That is the worst outcome this product has, and until this
34
+ * module every signal the CLI printed said it had worked.
35
+ *
36
+ * ## The two questions, and why they are both asked
37
+ *
38
+ * `seedVerdict` asks BEFORE launching: is the client's commit the one the image baked?
39
+ * `verifyVerdict` asks AFTER: did a server actually come up? Neither replaces the other.
40
+ * The first cannot be conclusive (the mirror may well serve a drifted client, and does
41
+ * for VS Code), and the second cannot be prompt — it costs the candidate up to a minute
42
+ * of waiting and, on a refusal, they have already opened a window. Asking both means the
43
+ * common, knowable failure is refused in advance and every other one is still loud.
44
+ *
45
+ * ## Why the probe is one shell command, and what it costs
46
+ *
47
+ * Each probe is `ssh <alias> <command>`, which sshd serves by exec'ing the candidate's
48
+ * login shell — so `/etc/ld.so.preload`'s `libttee.so` tees it and it lands in
49
+ * `candidate_activity_logs` as a `terminal_command`. That is acceptable and is NOT
50
+ * silent: the probe carries no pty, so `libttee`'s `tty_origin()` stamps it
51
+ * `origin: "tool"` (ENG-2179), which the read side groups as machinery rather than as
52
+ * something the candidate typed, and which the idle clock (ENG-2167) reads as a
53
+ * connection and never as work. It is still a row per probe, so the schedule below is
54
+ * deliberately sparse and backs off rather than polling on a fixed short interval.
55
+ */
56
+ /** What one probe of the workspace answered. Every field is three-valued on purpose. */
57
+ export interface WorkspaceProbe {
58
+ /**
59
+ * The server commit the IMAGE baked, from the cache pointer, or null when the pointer
60
+ * is absent or unreadable. Null is "we do not know", never "there is no bundle".
61
+ */
62
+ seedCommit: string | null;
63
+ /** How many processes the workspace is running out of this editor's server tree. */
64
+ serverProcesses: number;
65
+ /**
66
+ * Newest entry under `<serverHome>/data/logs`, or null when that directory does not
67
+ * exist. The names are timestamps (`20260915T200622`), so lexicographic order is
68
+ * chronological order and no `stat` is needed.
69
+ */
70
+ newestLogDir: string | null;
71
+ }
72
+ /**
73
+ * The command a probe runs in the workspace. POSIX `sh` only, and built from two
74
+ * constants we own — never from anything a candidate or a company supplies.
75
+ *
76
+ * Four details are load-bearing, and three of them are about one thing: `ps` output is
77
+ * full of near-misses, and a false POSITIVE here certifies a window that records nothing.
78
+ *
79
+ * The LEADING DOT keeps the pattern off the browser IDE — `~/.openvscode-server` contains
80
+ * `vscode-server` but not `/.vscode-server/`, and the browser IDE's node process is
81
+ * running on every healthy container, so a pattern without the dot answers "connected"
82
+ * for a native editor that never attached. The `[.]` bracket keeps it off the `grep` in
83
+ * that same listing, and the ANCHOR keeps it off everything that merely names the path.
84
+ * See the comments in the body for the measurements behind each.
85
+ *
86
+ * THE HOME SPELLING IS NOT THE ONLY ONE A HEALTHY CONTAINER PRODUCES, and missing the
87
+ * other one is silent. `litmus-firstboot.sh`'s `prewarm_editor` installs the baked bundle
88
+ * as a SYMLINK — `~/.<editor>-server/cli/servers/Stable-<commit>/server` ->
89
+ * `/opt/<editor>-server-cache/<commit>` — and the VS Code-family server bootstrap
90
+ * resolves its own root with `readlink -f "$0"` before exec'ing node, so on a PREWARMED
91
+ * (commit-matching, i.e. healthy) container argv[0] can be the `/opt` path. This repo
92
+ * already records both spellings for exactly these trees:
93
+ * `frontend/lib/server/tool-command-origin.ts`'s `SYSTEM_EDITOR_MARKERS` lists
94
+ * `/opt/vscode-server-cache/` and `/opt/cursor-server-cache/` beside `/.vscode-server/`.
95
+ * So `serverProcessPattern` carries both as alternatives, and every property above still
96
+ * holds of the added one: it is anchored at argv[0], so a candidate's `ls
97
+ * /opt/cursor-server-cache` is still not a server; it still cannot self-match, because
98
+ * the probe's own argv[0] is `sh` and the cache root reaches the command only as a shell
99
+ * VARIABLE, never as a literal followed by a slash; and unlike the home spelling that
100
+ * tree is root-owned on a read-only rootfs, so a candidate cannot put a binary there to
101
+ * forge one.
102
+ *
103
+ * WHAT IS STILL UNMEASURED, stated rather than implied: the controls behind this pattern
104
+ * are synthetic argv lines, taken on a laptop and not against a real prewarmed container.
105
+ * `ps -eo args=` on a live attached workspace, for both editors, is still to be confirmed.
106
+ * Adding the alternative is the safe direction in the meantime — it can only restore a
107
+ * signal that would otherwise be silent on the HEALTHY path, and the failure it prevents
108
+ * is the false alarm `verifyVerdict` names: a candidate who re-runs `litmus connect` with
109
+ * a window already open joins the running server, writes no new log directory, and would
110
+ * be told their work is not recorded.
111
+ *
112
+ * The banner is printed FIRST and unconditionally. `ssh` mixes the remote command's
113
+ * stdout with anything the login shell's rc files print, and a candidate's own `.bashrc`
114
+ * can write whatever it likes; a run that did not reach the banner is not a reading.
115
+ *
116
+ * `ps -eo args=` and not `pgrep`: procps is what the container has (the browser IDE's own
117
+ * monitor loop runs `ps -ax -o …`, ENG-2143), and `pgrep` is not guaranteed. A missing
118
+ * `ps` yields `proc 0`, which is the safe direction — it can only withhold a "connected"
119
+ * verdict, never manufacture one.
120
+ */
121
+ /**
122
+ * The argv[0] pattern the probe hands `grep -E`, as ONE derivation used two ways.
123
+ *
124
+ * `remoteProbeCommand` calls it with the shell EXPRESSIONS that expand to the two names
125
+ * (`${d#.}` and `${c}`), so the alternation reaches the container as a pattern built from
126
+ * shell variables; a test calls it with the literal names and gets the ERE the container
127
+ * will actually run. Keeping both behind one function is what stops the anchor or the
128
+ * alternation being right in one form and wrong in the other.
129
+ */
130
+ export declare function serverProcessPattern(serverHomeName: string, cacheRoot: string): string;
131
+ /**
132
+ * The names under `<serverHome>/data/logs` this probe will consider, as an ERE.
133
+ *
134
+ * The maximum is taken lexicographically, which is chronological ONLY over timestamps —
135
+ * and in C collation every letter sorts above every digit, so one stray `.log`, lock file
136
+ * or future named subdirectory becomes the maximum forever. The direction that breaks in
137
+ * is the bad one: `newestLogDir` stops advancing, the log signal dies silently, and a
138
+ * genuinely attached editor reads as `not-connected` — the false alarm at a working
139
+ * candidate that the process signal exists to prevent. So the invariant the field's
140
+ * docstring states is ENFORCED here rather than assumed, and a directory holding nothing
141
+ * timestamp-shaped answers "no logs", which is a missing input.
142
+ *
143
+ * The prefix is anchored and the tail is not: a build that appends a suffix to the
144
+ * timestamp still sorts chronologically, while a name that does not START with one cannot
145
+ * poison the maximum.
146
+ */
147
+ export declare const LOG_DIR_NAME_ERE = "^[0-9]{8}T[0-9]{6}";
148
+ export declare function remoteProbeCommand(serverHomeDir: string, seedCommitPath: string): string;
149
+ /**
150
+ * Read a probe's stdout. Returns null for anything that is not a complete reading —
151
+ * a missing banner, a missing field, a non-commit `seed` — because a partial probe that
152
+ * degrades into a WorkspaceProbe with plausible zeros is a probe that reports "no server
153
+ * is running" when what happened is that the probe did not run.
154
+ */
155
+ export declare function parseWorkspaceProbe(stdout: string | null): WorkspaceProbe | null;
156
+ /** What `<editor> --version` says about itself. */
157
+ export interface EditorVersion {
158
+ version: string | null;
159
+ commit: string | null;
160
+ }
161
+ /**
162
+ * Read `code --version` / `cursor --version`. Both print exactly three lines — version,
163
+ * commit, arch — and NEITHER prints a product name (the measurement is in
164
+ * `NATIVE_EDITORS`' block comment in `connect.ts`, and is why identity is read off
165
+ * `--help` instead). The commit is found by SHAPE rather than by line number, so a build
166
+ * that adds a line does not silently shift which field we read; the version is the first
167
+ * line that is not the commit.
168
+ */
169
+ export declare function parseEditorVersion(stdout: string | null): EditorVersion;
170
+ /**
171
+ * Whether the client will find its own server already in the workspace.
172
+ *
173
+ * `unknown` is the answer to every missing input, and the direction that fails in is the
174
+ * point. We can positively assert only one thing — "these two commits are both readable
175
+ * and they differ" — so an unreadable `--version`, an unreachable workspace, an image
176
+ * with no pointer and an editor that names its build some other way may only move the
177
+ * answer towards "we could not tell", never towards a refusal. Refusing on a probe that
178
+ * failed for its own reasons would take a working editor away from a candidate on the
179
+ * strength of nothing; proceeding leaves them where they were before this existed, and
180
+ * `verifyVerdict` below is what still makes that loud.
181
+ */
182
+ export type SeedVerdict = {
183
+ kind: "match";
184
+ commit: string;
185
+ } | {
186
+ kind: "mismatch";
187
+ clientCommit: string;
188
+ clientVersion: string | null;
189
+ seedCommit: string;
190
+ } | {
191
+ kind: "unknown";
192
+ reason: "no-client-commit" | "no-workspace-reading" | "no-seed";
193
+ };
194
+ export declare function seedVerdict(client: EditorVersion, probe: WorkspaceProbe | null): SeedVerdict;
195
+ /**
196
+ * Did a server come up because of THIS launch?
197
+ *
198
+ * THE TWO SIGNALS ARE READ DIFFERENTLY, and which is which is the whole of this.
199
+ *
200
+ * A LOG DIRECTORY IS AN ARTIFACT ON DISK, so it must be NEW. The home mount persists
201
+ * across park, resume and re-provision, so `<serverHome>/data/logs` from a connection
202
+ * last Tuesday is still sitting there today; a verdict keyed on its mere existence
203
+ * answers "connected" for the exact candidate this ticket is about — one whose second
204
+ * `litmus connect` attaches to nothing. That is why the caller takes a BASELINE probe
205
+ * before launching and hands it back here.
206
+ *
207
+ * SO A MISSING BASELINE DISABLES THAT SIGNAL ENTIRELY, and reading it as "there was no
208
+ * log directory before" is the same false reassurance arriving by a different road. The
209
+ * baseline probe is one `ssh`, and every way it can fail — a flaky link, a key that
210
+ * wants a passphrase, a container mid-resume — answers null; a candidate who has
211
+ * connected to this workspace before still has last Tuesday's `data/logs` sitting in the
212
+ * persistent home, so a null baseline plus a dead launch would be certified "connected"
213
+ * on an artifact this launch did not create. A missing input may only move the answer
214
+ * towards "we could not tell", which is the posture every other verdict in this module
215
+ * already takes, so with no baseline only a RUNNING PROCESS may conclude anything and
216
+ * everything else is `unverified` — never `not-connected`, because we have no reading to
217
+ * call anything new against.
218
+ *
219
+ * And the comparison is `>` rather than `!==`: the names are timestamps, so a directory
220
+ * that merely CHANGED (an editor pruning its own logs, a name that sorts earlier) is not
221
+ * evidence that this launch wrote one.
222
+ *
223
+ * A RUNNING PROCESS IS A STATEMENT ABOUT NOW, and needs no baseline. Park is `docker
224
+ * stop`, so every process in the container dies with it and one that is running belongs
225
+ * to this container's current run — nothing stale can be seen here. Requiring it to be
226
+ * new as well is what an earlier cut of this did, and it is wrong in the direction that
227
+ * matters: a candidate who already has a window open and runs `litmus connect` again
228
+ * gets a second window attached to the SAME server, creating no process and no log
229
+ * directory, and would have been told their work was not being recorded. Telling a
230
+ * working candidate to abandon their editor is worse than the residual, which is narrow
231
+ * and named: a server already up that this particular window fails to join reads as
232
+ * connected. For Cursor the one known cause of that — a build mismatch — has already
233
+ * refused the launch before this is ever asked.
234
+ */
235
+ export type VerifyVerdict = {
236
+ kind: "connected";
237
+ via: "process" | "logs";
238
+ } | {
239
+ kind: "not-connected";
240
+ } | {
241
+ kind: "unverified";
242
+ };
243
+ export declare function verifyVerdict(baseline: WorkspaceProbe | null, current: WorkspaceProbe | null): VerifyVerdict;
244
+ /**
245
+ * When to probe, in milliseconds after the launch, and why it is a list rather than an
246
+ * interval. Every entry costs one `terminal_command` row in the candidate's own activity
247
+ * record (see this module's header), so the schedule spends its probes where the answer
248
+ * changes — a cache hit attaches in seconds — and backs off rather than paying twenty
249
+ * rows to shave a second off a case that was going to fail anyway. The last entry IS the
250
+ * budget: ~60s, which is the ticket's bound and comfortably past a warm attach.
251
+ */
252
+ export declare const VERIFY_SCHEDULE_MS: readonly number[];
253
+ //# sourceMappingURL=editor-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor-server.d.ts","sourceRoot":"","sources":["../../src/lib/editor-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,oFAAoF;IACpF,eAAe,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB,uBAAuB,CAAA;AAEpD,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAkDxF;AAKD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,GAAG,IAAI,CAwBhF;AAED,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,aAAa,CASvE;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,kBAAkB,GAAG,sBAAsB,GAAG,SAAS,CAAA;CAAE,CAAA;AAExF,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,GAAG,WAAW,CAW5F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,GACzB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAA;AAE1B,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,aAAa,CAU5G;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAA2D,CAAA"}
@@ -0,0 +1,286 @@
1
+ /**
2
+ * ENG-2332 — whether a native editor can, and then DID, start its server inside the
3
+ * workspace. Everything here is pure or takes its one side effect as an argument, so the
4
+ * decisions are testable without a Cursor install and without a live container.
5
+ *
6
+ * ## The failure this exists to stop
7
+ *
8
+ * A VS Code-family editor opening a Remote-SSH window does not run anything of ours: it
9
+ * copies its OWN server build into the remote home and launches it there. Which build is
10
+ * decided by the CLIENT, by commit, and the two editors are pinned to each other — a
11
+ * 3.19.13 client will not talk to a 3.20.21 server, it fetches its own.
12
+ *
13
+ * A Codespaces v2 container has sealed egress. It cannot reach `api2.cursor.sh`,
14
+ * `downloads.cursor.com` or `github.com` (measured from inside a prod workspace,
15
+ * 2026-09-15: `curl` returns 000 after a 12s timeout). The only route a non-baked client
16
+ * has is the mirror (`infra/vscode-mirror/`, ENG-1062 for VS Code, ENG-1265 + ENG-1936
17
+ * for Cursor), and a mirror route that has stopped matching what a client asks for is
18
+ * indistinguishable, from the client's side, from no route at all.
19
+ *
20
+ * What the client does then is the whole problem: it gives up and opens the window
21
+ * anyway. MEASURED on macOS against a live prod workspace (ENG-2332):
22
+ *
23
+ * - `cursor --remote ssh-remote+<alias> <dir>` exits 0 and prints nothing.
24
+ * - A window appears, carrying the remote authority — `storage.json` records
25
+ * `"remoteAuthority": "ssh-remote+<alias>"` — and Cursor's ordinary agent home
26
+ * screen. It is indistinguishable from a working one.
27
+ * - The renderer log says `Started local extension host`, and every
28
+ * `$updateShellExecCapabilities` is refused as `non-authoritative host`.
29
+ * - Nothing under `~/.cursor-server` on the VM is touched. No `data/logs` is created.
30
+ * No server process exists.
31
+ *
32
+ * So the candidate works in what looks like their editor, on their own laptop's files,
33
+ * and NOTHING IS RECORDED. That is the worst outcome this product has, and until this
34
+ * module every signal the CLI printed said it had worked.
35
+ *
36
+ * ## The two questions, and why they are both asked
37
+ *
38
+ * `seedVerdict` asks BEFORE launching: is the client's commit the one the image baked?
39
+ * `verifyVerdict` asks AFTER: did a server actually come up? Neither replaces the other.
40
+ * The first cannot be conclusive (the mirror may well serve a drifted client, and does
41
+ * for VS Code), and the second cannot be prompt — it costs the candidate up to a minute
42
+ * of waiting and, on a refusal, they have already opened a window. Asking both means the
43
+ * common, knowable failure is refused in advance and every other one is still loud.
44
+ *
45
+ * ## Why the probe is one shell command, and what it costs
46
+ *
47
+ * Each probe is `ssh <alias> <command>`, which sshd serves by exec'ing the candidate's
48
+ * login shell — so `/etc/ld.so.preload`'s `libttee.so` tees it and it lands in
49
+ * `candidate_activity_logs` as a `terminal_command`. That is acceptable and is NOT
50
+ * silent: the probe carries no pty, so `libttee`'s `tty_origin()` stamps it
51
+ * `origin: "tool"` (ENG-2179), which the read side groups as machinery rather than as
52
+ * something the candidate typed, and which the idle clock (ENG-2167) reads as a
53
+ * connection and never as work. It is still a row per probe, so the schedule below is
54
+ * deliberately sparse and backs off rather than polling on a fixed short interval.
55
+ */
56
+ /** A probe line's shape, so a truncated or interleaved read is refused rather than parsed. */
57
+ const PROBE_BANNER = "litmus-probe 1";
58
+ /**
59
+ * The command a probe runs in the workspace. POSIX `sh` only, and built from two
60
+ * constants we own — never from anything a candidate or a company supplies.
61
+ *
62
+ * Four details are load-bearing, and three of them are about one thing: `ps` output is
63
+ * full of near-misses, and a false POSITIVE here certifies a window that records nothing.
64
+ *
65
+ * The LEADING DOT keeps the pattern off the browser IDE — `~/.openvscode-server` contains
66
+ * `vscode-server` but not `/.vscode-server/`, and the browser IDE's node process is
67
+ * running on every healthy container, so a pattern without the dot answers "connected"
68
+ * for a native editor that never attached. The `[.]` bracket keeps it off the `grep` in
69
+ * that same listing, and the ANCHOR keeps it off everything that merely names the path.
70
+ * See the comments in the body for the measurements behind each.
71
+ *
72
+ * THE HOME SPELLING IS NOT THE ONLY ONE A HEALTHY CONTAINER PRODUCES, and missing the
73
+ * other one is silent. `litmus-firstboot.sh`'s `prewarm_editor` installs the baked bundle
74
+ * as a SYMLINK — `~/.<editor>-server/cli/servers/Stable-<commit>/server` ->
75
+ * `/opt/<editor>-server-cache/<commit>` — and the VS Code-family server bootstrap
76
+ * resolves its own root with `readlink -f "$0"` before exec'ing node, so on a PREWARMED
77
+ * (commit-matching, i.e. healthy) container argv[0] can be the `/opt` path. This repo
78
+ * already records both spellings for exactly these trees:
79
+ * `frontend/lib/server/tool-command-origin.ts`'s `SYSTEM_EDITOR_MARKERS` lists
80
+ * `/opt/vscode-server-cache/` and `/opt/cursor-server-cache/` beside `/.vscode-server/`.
81
+ * So `serverProcessPattern` carries both as alternatives, and every property above still
82
+ * holds of the added one: it is anchored at argv[0], so a candidate's `ls
83
+ * /opt/cursor-server-cache` is still not a server; it still cannot self-match, because
84
+ * the probe's own argv[0] is `sh` and the cache root reaches the command only as a shell
85
+ * VARIABLE, never as a literal followed by a slash; and unlike the home spelling that
86
+ * tree is root-owned on a read-only rootfs, so a candidate cannot put a binary there to
87
+ * forge one.
88
+ *
89
+ * WHAT IS STILL UNMEASURED, stated rather than implied: the controls behind this pattern
90
+ * are synthetic argv lines, taken on a laptop and not against a real prewarmed container.
91
+ * `ps -eo args=` on a live attached workspace, for both editors, is still to be confirmed.
92
+ * Adding the alternative is the safe direction in the meantime — it can only restore a
93
+ * signal that would otherwise be silent on the HEALTHY path, and the failure it prevents
94
+ * is the false alarm `verifyVerdict` names: a candidate who re-runs `litmus connect` with
95
+ * a window already open joins the running server, writes no new log directory, and would
96
+ * be told their work is not recorded.
97
+ *
98
+ * The banner is printed FIRST and unconditionally. `ssh` mixes the remote command's
99
+ * stdout with anything the login shell's rc files print, and a candidate's own `.bashrc`
100
+ * can write whatever it likes; a run that did not reach the banner is not a reading.
101
+ *
102
+ * `ps -eo args=` and not `pgrep`: procps is what the container has (the browser IDE's own
103
+ * monitor loop runs `ps -ax -o …`, ENG-2143), and `pgrep` is not guaranteed. A missing
104
+ * `ps` yields `proc 0`, which is the safe direction — it can only withhold a "connected"
105
+ * verdict, never manufacture one.
106
+ */
107
+ /**
108
+ * The argv[0] pattern the probe hands `grep -E`, as ONE derivation used two ways.
109
+ *
110
+ * `remoteProbeCommand` calls it with the shell EXPRESSIONS that expand to the two names
111
+ * (`${d#.}` and `${c}`), so the alternation reaches the container as a pattern built from
112
+ * shell variables; a test calls it with the literal names and gets the ERE the container
113
+ * will actually run. Keeping both behind one function is what stops the anchor or the
114
+ * alternation being right in one form and wrong in the other.
115
+ */
116
+ export function serverProcessPattern(serverHomeName, cacheRoot) {
117
+ return `^[^ ]*(/[.]${serverHomeName}/|${cacheRoot}/)`;
118
+ }
119
+ /**
120
+ * The names under `<serverHome>/data/logs` this probe will consider, as an ERE.
121
+ *
122
+ * The maximum is taken lexicographically, which is chronological ONLY over timestamps —
123
+ * and in C collation every letter sorts above every digit, so one stray `.log`, lock file
124
+ * or future named subdirectory becomes the maximum forever. The direction that breaks in
125
+ * is the bad one: `newestLogDir` stops advancing, the log signal dies silently, and a
126
+ * genuinely attached editor reads as `not-connected` — the false alarm at a working
127
+ * candidate that the process signal exists to prevent. So the invariant the field's
128
+ * docstring states is ENFORCED here rather than assumed, and a directory holding nothing
129
+ * timestamp-shaped answers "no logs", which is a missing input.
130
+ *
131
+ * The prefix is anchored and the tail is not: a build that appends a suffix to the
132
+ * timestamp still sorts chronologically, while a name that does not START with one cannot
133
+ * poison the maximum.
134
+ */
135
+ export const LOG_DIR_NAME_ERE = "^[0-9]{8}T[0-9]{6}";
136
+ export function remoteProbeCommand(serverHomeDir, seedCommitPath) {
137
+ // Both arguments are module constants in `connect.ts`. Assert that rather than trust
138
+ // it: a probe body is a shell program, and the day one of these becomes derived from
139
+ // anything else, this is the line that has to fail instead of interpolating.
140
+ if (!/^\.[a-z0-9-]+$/.test(serverHomeDir))
141
+ throw new Error(`unsafe server home dir: ${serverHomeDir}`);
142
+ if (!/^\/[A-Za-z0-9/_.-]+$/.test(seedCommitPath))
143
+ throw new Error(`unsafe seed path: ${seedCommitPath}`);
144
+ // The baked bundle and its pointer are ONE tree: `/opt/<editor>-server-cache/COMMIT`
145
+ // sits inside `/opt/<editor>-server-cache/`. Deriving the root from the pointer rather
146
+ // than declaring it again keeps the record in `connect.ts` the only place either is
147
+ // spelled — a second literal is a copy that goes stale silently, which is exactly what
148
+ // `backend/tests/test_editor_server_paths.py` pins the first one against.
149
+ const cacheRoot = seedCommitPath.replace(/\/[^/]+$/, "");
150
+ if (!/^\/[A-Za-z0-9/_.-]+$/.test(cacheRoot))
151
+ throw new Error(`unsafe seed path: ${seedCommitPath}`);
152
+ // THE COMMAND MUST NOT CONTAIN THE STRING IT SEARCHES FOR. `ps -eo args=` lists this
153
+ // probe's own `sh -c <command>` process, so a body carrying a literal
154
+ // `/.cursor-server/` — which the obvious spelling of the logs path does — matches
155
+ // itself. MEASURED: the first cut of this returned `proc 4` on a laptop with no server
156
+ // of any kind running. The consequence is not a false "connected", because the BASELINE
157
+ // probe matches itself too and `verifyVerdict` then sees a process that was already
158
+ // there; it is worse than that — the process signal quietly stops being a signal, and
159
+ // the whole verification silently rests on the log directory alone.
160
+ //
161
+ // So the directory name is held in `$d` and never written next to a slash, and the
162
+ // pattern keeps `[.]` (which also stops it matching the `grep` in that same listing).
163
+ // `${d#.}` strips the leading dot so the bracket can supply it.
164
+ //
165
+ // AND THE PATTERN IS ANCHORED AT ARGV[0], because the dangerous direction here is a
166
+ // false POSITIVE: a probe that certifies a dead window is worse than one that says
167
+ // nothing. A candidate's own `ls ~/.cursor-server`, a `grep` over it, an editor's file
168
+ // watcher — any of them puts that path in a `ps` listing while proving nothing about a
169
+ // server. `^[^ ]*` requires the path to be the EXECUTABLE, which it is for the server
170
+ // (its node binary lives inside that tree) and is not for anything merely naming it.
171
+ // A server launched some other way loses this signal and keeps the log one, which is
172
+ // the safe way for it to be wrong.
173
+ const logs = `"$HOME/$d/data/logs"`;
174
+ // Both names reach the pattern as shell VARIABLES, never as literals followed by a
175
+ // slash, so the command text still cannot hold the string it searches for.
176
+ const pattern = serverProcessPattern("${d#.}", "${c}");
177
+ return [
178
+ `printf '${PROBE_BANNER}\\n';`,
179
+ `d=${serverHomeDir};`,
180
+ `c=${cacheRoot};`,
181
+ `if [ -r '${seedCommitPath}' ]; then`,
182
+ `printf 'seed %s\\n' "$(tr -d '\\r\\n' < '${seedCommitPath}' 2>/dev/null)";`,
183
+ `else printf 'seed -\\n'; fi;`,
184
+ `printf 'proc %s\\n' "$(ps -eo args= 2>/dev/null | grep -Ec "${pattern}" || true)";`,
185
+ `if [ -d ${logs} ]; then`,
186
+ `printf 'log %s\\n' "$(ls -1 ${logs} 2>/dev/null | grep -E '${LOG_DIR_NAME_ERE}' | sort | tail -n 1)";`,
187
+ `else printf 'log -\\n'; fi`,
188
+ ].join(" ");
189
+ }
190
+ /** A 40-hex commit, the only shape either editor names a server build by. */
191
+ const COMMIT_RE = /^[0-9a-f]{40}$/;
192
+ /**
193
+ * Read a probe's stdout. Returns null for anything that is not a complete reading —
194
+ * a missing banner, a missing field, a non-commit `seed` — because a partial probe that
195
+ * degrades into a WorkspaceProbe with plausible zeros is a probe that reports "no server
196
+ * is running" when what happened is that the probe did not run.
197
+ */
198
+ export function parseWorkspaceProbe(stdout) {
199
+ if (!stdout)
200
+ return null;
201
+ const lines = stdout.split(/\r?\n/).map((l) => l.trim());
202
+ if (!lines.includes(PROBE_BANNER))
203
+ return null;
204
+ const field = (name) => {
205
+ // LAST match wins: an rc file that echoes something shaped like a field cannot
206
+ // displace ours, because ours are printed after the shell has finished starting.
207
+ for (let i = lines.length - 1; i >= 0; i--) {
208
+ if (lines[i] === name || lines[i].startsWith(`${name} `))
209
+ return lines[i].slice(name.length).trim();
210
+ }
211
+ return null;
212
+ };
213
+ const seedRaw = field("seed");
214
+ const procRaw = field("proc");
215
+ const logRaw = field("log");
216
+ if (seedRaw === null || procRaw === null || logRaw === null)
217
+ return null;
218
+ const proc = Number.parseInt(procRaw, 10);
219
+ if (!Number.isFinite(proc) || proc < 0)
220
+ return null;
221
+ const seedCommit = COMMIT_RE.test(seedRaw) ? seedRaw : null;
222
+ return {
223
+ seedCommit,
224
+ serverProcesses: proc,
225
+ newestLogDir: logRaw === "-" || logRaw === "" ? null : logRaw,
226
+ };
227
+ }
228
+ /**
229
+ * Read `code --version` / `cursor --version`. Both print exactly three lines — version,
230
+ * commit, arch — and NEITHER prints a product name (the measurement is in
231
+ * `NATIVE_EDITORS`' block comment in `connect.ts`, and is why identity is read off
232
+ * `--help` instead). The commit is found by SHAPE rather than by line number, so a build
233
+ * that adds a line does not silently shift which field we read; the version is the first
234
+ * line that is not the commit.
235
+ */
236
+ export function parseEditorVersion(stdout) {
237
+ if (!stdout)
238
+ return { version: null, commit: null };
239
+ const lines = stdout
240
+ .split(/\r?\n/)
241
+ .map((l) => l.trim())
242
+ .filter((l) => l.length > 0);
243
+ const commit = lines.find((l) => COMMIT_RE.test(l)) ?? null;
244
+ const version = lines.find((l) => !COMMIT_RE.test(l) && /^\d+\.\d+/.test(l)) ?? null;
245
+ return { version, commit };
246
+ }
247
+ export function seedVerdict(client, probe) {
248
+ if (!client.commit)
249
+ return { kind: "unknown", reason: "no-client-commit" };
250
+ if (!probe)
251
+ return { kind: "unknown", reason: "no-workspace-reading" };
252
+ if (!probe.seedCommit)
253
+ return { kind: "unknown", reason: "no-seed" };
254
+ if (probe.seedCommit === client.commit)
255
+ return { kind: "match", commit: client.commit };
256
+ return {
257
+ kind: "mismatch",
258
+ clientCommit: client.commit,
259
+ clientVersion: client.version,
260
+ seedCommit: probe.seedCommit,
261
+ };
262
+ }
263
+ export function verifyVerdict(baseline, current) {
264
+ if (!current)
265
+ return { kind: "unverified" };
266
+ if (current.serverProcesses > 0)
267
+ return { kind: "connected", via: "process" };
268
+ if (!baseline)
269
+ return { kind: "unverified" };
270
+ if (current.newestLogDir !== null && current.newestLogDir > (baseline.newestLogDir ?? "")) {
271
+ return { kind: "connected", via: "logs" };
272
+ }
273
+ // No server running and a log directory that has not advanced is the measured failure:
274
+ // the client gave up before writing anything to the workspace at all.
275
+ return { kind: "not-connected" };
276
+ }
277
+ /**
278
+ * When to probe, in milliseconds after the launch, and why it is a list rather than an
279
+ * interval. Every entry costs one `terminal_command` row in the candidate's own activity
280
+ * record (see this module's header), so the schedule spends its probes where the answer
281
+ * changes — a cache hit attaches in seconds — and backs off rather than paying twenty
282
+ * rows to shave a second off a case that was going to fail anyway. The last entry IS the
283
+ * budget: ~60s, which is the ticket's bound and comfortably past a warm attach.
284
+ */
285
+ export const VERIFY_SCHEDULE_MS = [4000, 9000, 15000, 23000, 33000, 45000, 60000];
286
+ //# sourceMappingURL=editor-server.js.map