@tsconfigdotjson/vibe-os 0.2.0-rc.1 → 0.3.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/README.md +19 -847
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/web/assets/index-82MhMOB4.css +1 -0
- package/dist/web/assets/index-82MhMOB4.css.br +0 -0
- package/dist/web/assets/index-82MhMOB4.css.gz +0 -0
- package/dist/web/assets/index-DCB2nO0u.js +36 -0
- package/dist/web/assets/index-DCB2nO0u.js.br +0 -0
- package/dist/web/assets/index-DCB2nO0u.js.gz +0 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/scripts/notices.ts +3 -1
- package/server/api.ts +449 -20
- package/server/assets.generated.ts +13 -13
- package/server/attach.ts +65 -16
- package/server/browser.ts +1 -65
- package/server/cli.ts +48 -150
- package/server/client.ts +440 -0
- package/server/config.ts +64 -2
- package/server/db.ts +19 -0
- package/server/doctor.ts +176 -123
- package/server/harness.ts +378 -590
- package/server/harnesses/claude.ts +248 -0
- package/server/harnesses/codex.ts +241 -0
- package/server/harnesses/cursor.ts +202 -0
- package/server/harnesses/hermes.ts +259 -0
- package/server/harnesses/util.ts +181 -0
- package/server/headless.ts +358 -0
- package/server/index.ts +47 -14
- package/server/memory.ts +418 -0
- package/server/profiles.ts +27 -15
- package/server/projects.ts +52 -3
- package/server/reaper.ts +5 -3
- package/server/screens.ts +372 -0
- package/server/session.ts +84 -95
- package/server/setup.ts +73 -7
- package/server/windows.ts +51 -1
- package/shared/blanks.ts +113 -0
- package/shared/harness.ts +375 -0
- package/shared/wire.ts +105 -94
- package/dist/web/assets/index-B77Qk8B5.js +0 -35
- package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
- package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css +0 -1
- package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
package/server/doctor.ts
CHANGED
|
@@ -25,14 +25,14 @@ import {
|
|
|
25
25
|
vncPasswordFileFromUnit,
|
|
26
26
|
} from "./browser.ts";
|
|
27
27
|
import type { Config } from "./config.ts";
|
|
28
|
+
import { discover, harnesses, harnessProblems } from "./harness.ts";
|
|
28
29
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "./harness.ts";
|
|
30
|
+
type MemoryLimits,
|
|
31
|
+
type OomState,
|
|
32
|
+
oomState,
|
|
33
|
+
type ScopeState,
|
|
34
|
+
scopeState,
|
|
35
|
+
} from "./memory.ts";
|
|
36
36
|
import { findLeaks } from "./reaper.ts";
|
|
37
37
|
import { IS_COMPILED } from "./runtime.ts";
|
|
38
38
|
import { discoverHostKey, SshCa } from "./ssh-ca.ts";
|
|
@@ -109,7 +109,7 @@ async function firewallState(): Promise<FirewallState> {
|
|
|
109
109
|
directUdpAllowed: false,
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
// ufw first: it is what
|
|
112
|
+
// ufw first: it is what docs/deploying.md documents, and its status output says
|
|
113
113
|
// plainly whether it is switched on, which a raw ruleset does not.
|
|
114
114
|
const ufw = await run("ufw", ["status", "verbose"], { timeout: 5000 })
|
|
115
115
|
.then((r) => r.stdout)
|
|
@@ -349,114 +349,60 @@ async function browserChecks(): Promise<Check[]> {
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
/**
|
|
352
|
-
*
|
|
352
|
+
* Every harness: installed, logged in, and whatever else its adapter checks.
|
|
353
353
|
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
354
|
+
* None of it is fatal. A box that never installs a given harness is a working
|
|
355
|
+
* box; only the profiles that use it are affected. The login check exists
|
|
356
|
+
* because its failure is quiet in exactly the way this file cares about: the
|
|
357
|
+
* window opens, the harness runs, and it sits at a login prompt.
|
|
357
358
|
*/
|
|
358
|
-
async function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
369
|
-
];
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const checks: Check[] = [
|
|
373
|
-
ok(
|
|
374
|
-
"hermes",
|
|
375
|
-
`${hermes.version ?? "installed"} — the Hermes harness will run`,
|
|
359
|
+
async function harnessChecks(): Promise<Check[]> {
|
|
360
|
+
const checks: Check[] = [];
|
|
361
|
+
for (const { file, error } of harnessProblems())
|
|
362
|
+
checks.push(bad("harness file", `${file}: ${error} — skipped`));
|
|
363
|
+
|
|
364
|
+
// Concurrently: each is its own few process starts, and the slow ones go to
|
|
365
|
+
// the network.
|
|
366
|
+
const adapters = harnesses();
|
|
367
|
+
const found = await Promise.all(
|
|
368
|
+
adapters.map(
|
|
369
|
+
(a) => discover(a.spec.id) as NonNullable<ReturnType<typeof discover>>,
|
|
376
370
|
),
|
|
377
|
-
];
|
|
378
|
-
|
|
379
|
-
// Without the CLI, Browser Use mode silently does not engage. Hermes keeps
|
|
380
|
-
// working with its twelve built-in browser tools, so nothing looks wrong;
|
|
381
|
-
// you just pay for a dozen tool schemas in every request and never find out.
|
|
382
|
-
checks.push(
|
|
383
|
-
hermes.browser.browserUse
|
|
384
|
-
? ok("browser-use", "runnable — Hermes gets the single browser_exec tool")
|
|
385
|
-
: bad(
|
|
386
|
-
"browser-use",
|
|
387
|
-
"no browser-use or uvx on PATH — Hermes keeps its twelve built-in browser tools",
|
|
388
|
-
UV_INSTALL,
|
|
389
|
-
),
|
|
390
371
|
);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
? `browser.cdp_url is ${hermes.browser.cdpUrl}, not this box's Chrome on ${hermes.browser.cdpPort}`
|
|
405
|
-
: "browser.cdp_url is unset — Hermes will not use the browser running here",
|
|
406
|
-
"vibe-os connect-hermes",
|
|
407
|
-
),
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return checks;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Cursor, and whether a window opened as it would get past the login prompt.
|
|
416
|
-
*
|
|
417
|
-
* Two questions. Neither is fatal, for the reason the Hermes checks give: a box
|
|
418
|
-
* that never installs Cursor is a working box. The login check exists because
|
|
419
|
-
* its failure is quiet in exactly the way this file cares about — the window
|
|
420
|
-
* opens, the harness runs, and it sits asking for a browser login on a box that
|
|
421
|
-
* may not have one attached.
|
|
422
|
-
*/
|
|
423
|
-
async function cursorChecks(): Promise<Check[]> {
|
|
424
|
-
const cursor = await discoverCursor();
|
|
425
|
-
if (!cursor.available) {
|
|
426
|
-
return [
|
|
427
|
-
bad(
|
|
428
|
-
"cursor",
|
|
429
|
-
"not on PATH — profiles using the Cursor harness fall back to a shell",
|
|
430
|
-
CURSOR_INSTALL,
|
|
431
|
-
),
|
|
432
|
-
];
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const checks: Check[] = [
|
|
436
|
-
ok(
|
|
437
|
-
"cursor",
|
|
438
|
-
`${cursor.version ?? "installed"} — the Cursor harness will run`,
|
|
439
|
-
),
|
|
440
|
-
];
|
|
441
|
-
|
|
442
|
-
if (cursor.loggedIn === false) {
|
|
443
|
-
checks.push(
|
|
444
|
-
bad(
|
|
445
|
-
"cursor login",
|
|
446
|
-
"not logged in — Cursor windows will sit at a login prompt",
|
|
447
|
-
"cursor-agent login",
|
|
448
|
-
),
|
|
449
|
-
);
|
|
450
|
-
} else if (cursor.loggedIn === true) {
|
|
372
|
+
adapters.forEach((adapter, i) => {
|
|
373
|
+
const { spec } = adapter;
|
|
374
|
+
const { report, detail } = found[i];
|
|
375
|
+
if (!report.available) {
|
|
376
|
+
checks.push(
|
|
377
|
+
bad(
|
|
378
|
+
spec.id,
|
|
379
|
+
`not on PATH — profiles using the ${spec.label} harness open a window that closes immediately`,
|
|
380
|
+
spec.install,
|
|
381
|
+
),
|
|
382
|
+
);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
451
385
|
checks.push(
|
|
452
386
|
ok(
|
|
453
|
-
|
|
454
|
-
|
|
387
|
+
spec.id,
|
|
388
|
+
`${report.version ?? "installed"} — the ${spec.label} harness will run`,
|
|
455
389
|
),
|
|
456
390
|
);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
391
|
+
if (spec.login && report.loggedIn === false) {
|
|
392
|
+
checks.push(
|
|
393
|
+
bad(
|
|
394
|
+
`${spec.id} login`,
|
|
395
|
+
`not logged in — ${spec.label} windows will sit at a login prompt`,
|
|
396
|
+
spec.login.fix,
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
} else if (spec.login && report.loggedIn === true) {
|
|
400
|
+
checks.push(ok(`${spec.id} login`, "logged in"));
|
|
401
|
+
}
|
|
402
|
+
// null stays silent: status output this build cannot read is not a finding.
|
|
403
|
+
for (const c of adapter.checks?.(report, detail) ?? [])
|
|
404
|
+
checks.push(c.ok ? ok(c.label, c.detail) : bad(c.label, c.detail, c.fix));
|
|
405
|
+
});
|
|
460
406
|
return checks;
|
|
461
407
|
}
|
|
462
408
|
|
|
@@ -591,6 +537,110 @@ async function tailscaleAddress(): Promise<string | null> {
|
|
|
591
537
|
return null;
|
|
592
538
|
}
|
|
593
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Whether one session can take the box down, and whether anything would stop
|
|
542
|
+
* it if one did. Pure, so every combination is testable off a real box.
|
|
543
|
+
*/
|
|
544
|
+
export function memoryChecks(
|
|
545
|
+
user: string,
|
|
546
|
+
limits: MemoryLimits,
|
|
547
|
+
scope: ScopeState,
|
|
548
|
+
oom: OomState,
|
|
549
|
+
): Check[] {
|
|
550
|
+
const checks: Check[] = [];
|
|
551
|
+
const set = [
|
|
552
|
+
limits.high !== null ? `MemoryHigh=${limits.high}` : null,
|
|
553
|
+
limits.max !== null ? `MemoryMax=${limits.max}` : null,
|
|
554
|
+
limits.swapMax !== null ? `MemorySwapMax=${limits.swapMax}` : null,
|
|
555
|
+
].filter(Boolean);
|
|
556
|
+
|
|
557
|
+
if (set.length === 0) {
|
|
558
|
+
checks.push(
|
|
559
|
+
ok("memory limits", "off (--no-memory-limit): windows start unscoped"),
|
|
560
|
+
);
|
|
561
|
+
} else if (!scope.systemd) {
|
|
562
|
+
checks.push(
|
|
563
|
+
bad(
|
|
564
|
+
"memory limits",
|
|
565
|
+
"no systemd here, so windows start without their memory limits and one runaway agent can take the box down",
|
|
566
|
+
"run on a systemd host, or rely on the OOM daemon below",
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
} else if (!scope.linger) {
|
|
570
|
+
checks.push(
|
|
571
|
+
bad(
|
|
572
|
+
"memory limits",
|
|
573
|
+
`lingering is off for ${user}, so windows start without their memory limits (a scope would end when the last login did)`,
|
|
574
|
+
`sudo loginctl enable-linger ${user} # or vibe-os setup`,
|
|
575
|
+
),
|
|
576
|
+
);
|
|
577
|
+
} else if (!scope.cgroup2) {
|
|
578
|
+
checks.push(
|
|
579
|
+
bad(
|
|
580
|
+
"memory limits",
|
|
581
|
+
`this box uses cgroup v1, where a user's scopes cannot be limited, so ${set.join(" ")} is set but not enforced`,
|
|
582
|
+
"boot with systemd.unified_cgroup_hierarchy=1, or rely on the OOM daemon below",
|
|
583
|
+
),
|
|
584
|
+
);
|
|
585
|
+
} else if (scope.controllers && !scope.controllers.includes("memory")) {
|
|
586
|
+
checks.push(
|
|
587
|
+
bad(
|
|
588
|
+
"memory limits",
|
|
589
|
+
`the user manager is not delegated the memory controller (it has: ${scope.controllers.join(" ") || "none"}), so ${set.join(" ")} is set but not enforced`,
|
|
590
|
+
"sudo systemctl edit user@.service # [Service] Delegate=pids memory cpu",
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
} else {
|
|
594
|
+
checks.push(
|
|
595
|
+
ok(
|
|
596
|
+
"memory limits",
|
|
597
|
+
`each window runs in its own scope: ${set.join(" ")}`,
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const swap = oom.swapTotal > 0;
|
|
603
|
+
if (oom.oomd || oom.earlyoom === "active") {
|
|
604
|
+
checks.push(
|
|
605
|
+
ok(
|
|
606
|
+
"oom daemon",
|
|
607
|
+
`${oom.oomd ? "systemd-oomd" : "earlyoom"} is running, so a box out of memory recovers on its own`,
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
} else if (oom.earlyoom === "installed") {
|
|
611
|
+
checks.push(
|
|
612
|
+
bad(
|
|
613
|
+
"oom daemon",
|
|
614
|
+
"earlyoom is installed but not running",
|
|
615
|
+
"sudo systemctl enable --now earlyoom",
|
|
616
|
+
),
|
|
617
|
+
);
|
|
618
|
+
} else {
|
|
619
|
+
checks.push(
|
|
620
|
+
bad(
|
|
621
|
+
"oom daemon",
|
|
622
|
+
swap
|
|
623
|
+
? "swap and no OOM daemon: out of memory, this box thrashes for as long as it takes someone to notice, instead of killing one process"
|
|
624
|
+
: "no OOM daemon: out of memory, the kernel kills something only after the box has stalled",
|
|
625
|
+
"sudo apt install earlyoom # or vibe-os setup",
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return checks;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** A user's uid, or null when there is no such user. */
|
|
633
|
+
async function uidOf(user: string): Promise<number | null> {
|
|
634
|
+
if (user === os.userInfo().username) return process.getuid?.() ?? null;
|
|
635
|
+
try {
|
|
636
|
+
const { stdout } = await run("id", ["-u", user], { timeout: 5_000 });
|
|
637
|
+
const uid = Number(stdout.trim());
|
|
638
|
+
return Number.isInteger(uid) ? uid : null;
|
|
639
|
+
} catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
594
644
|
export async function runDoctor(config: Config): Promise<Check[]> {
|
|
595
645
|
const checks: Check[] = [];
|
|
596
646
|
const user = config.user;
|
|
@@ -643,19 +693,7 @@ export async function runDoctor(config: Config): Promise<Check[]> {
|
|
|
643
693
|
),
|
|
644
694
|
);
|
|
645
695
|
|
|
646
|
-
|
|
647
|
-
checks.push(
|
|
648
|
-
claude
|
|
649
|
-
? ok("claude", "on PATH — the Claude harness will run")
|
|
650
|
-
: bad(
|
|
651
|
-
"claude",
|
|
652
|
-
"not on PATH — profiles using the Claude harness fall back to a shell",
|
|
653
|
-
CLAUDE_INSTALL,
|
|
654
|
-
),
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
checks.push(...(await hermesChecks()));
|
|
658
|
-
checks.push(...(await cursorChecks()));
|
|
696
|
+
checks.push(...(await harnessChecks()));
|
|
659
697
|
|
|
660
698
|
// ── can a browser actually log in? ───────────────────────────────────────
|
|
661
699
|
const sshdUp = await probeTcp(config.sshHost, config.sshPort);
|
|
@@ -866,7 +904,7 @@ export async function runDoctor(config: Config): Promise<Check[]> {
|
|
|
866
904
|
bad(
|
|
867
905
|
"firewall",
|
|
868
906
|
"could not read any firewall — needs root, or none is installed",
|
|
869
|
-
"sudo vibe-os doctor (
|
|
907
|
+
"sudo vibe-os doctor (firewall rules: https://github.com/tsconfigdotjson/vibe-os/blob/main/docs/deploying.md#firewall)",
|
|
870
908
|
),
|
|
871
909
|
);
|
|
872
910
|
} else if (!fw.active) {
|
|
@@ -913,6 +951,21 @@ export async function runDoctor(config: Config): Promise<Check[]> {
|
|
|
913
951
|
// not be told about four things it does not have.
|
|
914
952
|
checks.push(...(await browserChecks()));
|
|
915
953
|
|
|
954
|
+
// ── memory ───────────────────────────────────────────────────────────────
|
|
955
|
+
//
|
|
956
|
+
// Linux only: the scope and both daemons are systemd and /proc things, and a
|
|
957
|
+
// Mac running vibe-os locally has none of them to report on.
|
|
958
|
+
if (process.platform === "linux") {
|
|
959
|
+
checks.push(
|
|
960
|
+
...memoryChecks(
|
|
961
|
+
user,
|
|
962
|
+
config.memory,
|
|
963
|
+
await scopeState(user, await uidOf(user)),
|
|
964
|
+
await oomState(),
|
|
965
|
+
),
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
916
969
|
// ── leaked processes ─────────────────────────────────────────────────────
|
|
917
970
|
//
|
|
918
971
|
// Processes that outlived their workspace or session: a cwd that is a
|
|
@@ -960,7 +1013,7 @@ export async function runDoctor(config: Config): Promise<Check[]> {
|
|
|
960
1013
|
bad(
|
|
961
1014
|
"exposure",
|
|
962
1015
|
`no token and bound to every interface — Tailscale is up (${tailnet}), but so is any public address`,
|
|
963
|
-
`bind to the tailnet only: --host ${tailnet} (
|
|
1016
|
+
`bind to the tailnet only: --host ${tailnet} (firewall rules: https://github.com/tsconfigdotjson/vibe-os/blob/main/docs/deploying.md#firewall)`,
|
|
964
1017
|
),
|
|
965
1018
|
);
|
|
966
1019
|
} else {
|