@linzumi/cli 0.0.15-beta → 0.0.17-beta

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 CHANGED
@@ -107,6 +107,11 @@ workspace/channel scope from the approval flow, trusts only the selected
107
107
  folder by default, and listens only to the approving human unless
108
108
  `--listen-user` is explicitly passed.
109
109
 
110
+ By default, the runner downloads the Kandan-approved `code-server`
111
+ runtime for your platform and verifies its checksum before enabling the
112
+ browser editor. Linux editor launches are wrapped with `bubblewrap`
113
+ (`bwrap`) for filesystem confinement.
114
+
110
115
  `linzumi claim` also prints `support_channel_url`. That channel is the
111
116
  shared onboarding room for the approving human, the claimed agent identity,
112
117
  and Linzumi support. Keep repository work in task threads; use the support
@@ -229,7 +234,7 @@ privileges as your shell. Every action is auditable from the thread.
229
234
  ## Pinning a version
230
235
 
231
236
  ```bash
232
- npm install -g @linzumi/cli@0.0.15-beta
237
+ npm install -g @linzumi/cli@0.0.17-beta
233
238
  linzumi --version
234
239
  ```
235
240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.15-beta",
3
+ "version": "0.0.17-beta",
4
4
  "description": "Linzumi CLI — point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -121,7 +121,7 @@ async function main(args: readonly string[]): Promise<void> {
121
121
  process.stdout.write(connectGuideText());
122
122
  return;
123
123
  case "version":
124
- process.stdout.write("linzumi 0.0.15-beta\n");
124
+ process.stdout.write("linzumi 0.0.17-beta\n");
125
125
  return;
126
126
  case "auth":
127
127
  await runAuthCommand(parsed.args);
@@ -614,7 +614,7 @@ export async function parseRunnerArgs(
614
614
  }
615
615
 
616
616
  if (values.get("version") === true) {
617
- process.stdout.write("linzumi 0.0.15-beta\n");
617
+ process.stdout.write("linzumi 0.0.17-beta\n");
618
618
  process.exit(0);
619
619
  }
620
620
 
@@ -14,7 +14,7 @@ import {
14
14
  writeFileSync,
15
15
  } from "node:fs";
16
16
  import { tmpdir } from "node:os";
17
- import { delimiter, dirname, join } from "node:path";
17
+ import { basename, delimiter, dirname, join } from "node:path";
18
18
  import { chooseLoopbackPort } from "./codexAppServer";
19
19
  import { resolveAllowedCwd } from "./localCapabilities";
20
20
  import type { InstalledEditorRuntime } from "./localEditorRuntime";
@@ -91,6 +91,7 @@ export type PrepareCodeServerLaunchOptions = {
91
91
  readonly extensionsDir?: string | undefined;
92
92
  readonly platform?: NodeJS.Platform | undefined;
93
93
  readonly sandboxExecBin?: string | undefined;
94
+ readonly bubblewrapBin?: string | undefined;
94
95
  readonly envPath?: string | undefined;
95
96
  };
96
97
 
@@ -365,8 +366,6 @@ export function codeServerArgs(
365
366
  "--disable-update-check",
366
367
  "--disable-workspace-trust",
367
368
  "--disable-getting-started-override",
368
- "--reconnection-grace-time",
369
- "10",
370
369
  "--app-name",
371
370
  "Kandan",
372
371
  "--user-data-dir",
@@ -414,20 +413,18 @@ export function prepareCodeServerLaunch(
414
413
  ): PrepareCodeServerLaunchResult {
415
414
  const platform = options.platform ?? process.platform;
416
415
 
416
+ if (platform === "linux") {
417
+ return prepareLinuxCodeServerLaunch(options);
418
+ }
419
+
417
420
  if (platform !== "darwin") {
418
- return {
419
- ok: false,
420
- reason: "local_editor_filesystem_sandbox_unavailable",
421
- };
421
+ return filesystemSandboxUnavailable();
422
422
  }
423
423
 
424
424
  const sandboxExecBin = options.sandboxExecBin ?? "/usr/bin/sandbox-exec";
425
425
 
426
426
  if (!existsSync(sandboxExecBin)) {
427
- return {
428
- ok: false,
429
- reason: "local_editor_filesystem_sandbox_unavailable",
430
- };
427
+ return filesystemSandboxUnavailable();
431
428
  }
432
429
 
433
430
  const codeServerExecutable = resolveCodeServerExecutable(
@@ -436,10 +433,7 @@ export function prepareCodeServerLaunch(
436
433
  );
437
434
 
438
435
  if (!codeServerExecutable.ok) {
439
- return {
440
- ok: false,
441
- reason: "local_editor_filesystem_sandbox_unavailable",
442
- };
436
+ return filesystemSandboxUnavailable();
443
437
  }
444
438
 
445
439
  return {
@@ -460,6 +454,96 @@ export function prepareCodeServerLaunch(
460
454
  };
461
455
  }
462
456
 
457
+ function prepareLinuxCodeServerLaunch(
458
+ options: PrepareCodeServerLaunchOptions,
459
+ ): PrepareCodeServerLaunchResult {
460
+ const bubblewrapExecutable = resolveCodeServerExecutable(
461
+ options.bubblewrapBin ?? "bwrap",
462
+ options.envPath ?? process.env.PATH ?? "",
463
+ );
464
+
465
+ if (!bubblewrapExecutable.ok) {
466
+ return filesystemSandboxUnavailable();
467
+ }
468
+
469
+ const codeServerExecutable = resolveCodeServerExecutable(
470
+ options.codeServerBin,
471
+ options.envPath ?? process.env.PATH ?? "",
472
+ );
473
+
474
+ if (!codeServerExecutable.ok) {
475
+ return filesystemSandboxUnavailable();
476
+ }
477
+
478
+ const readOnlyRoots = uniquePaths([
479
+ "/usr",
480
+ "/bin",
481
+ "/sbin",
482
+ "/lib",
483
+ "/lib64",
484
+ "/etc",
485
+ "/nix",
486
+ "/opt",
487
+ ...(options.codeServerRuntimeRoot === undefined
488
+ ? []
489
+ : sandboxPathAliases(options.codeServerRuntimeRoot)),
490
+ codeServerExecutable.directory,
491
+ ]);
492
+
493
+ const args = [
494
+ "--die-with-parent",
495
+ "--proc",
496
+ "/proc",
497
+ "--dev-bind",
498
+ "/dev",
499
+ "/dev",
500
+ "--tmpfs",
501
+ "/tmp",
502
+ "--setenv",
503
+ "HOME",
504
+ options.userDataDir,
505
+ "--setenv",
506
+ "XDG_DATA_HOME",
507
+ join(options.userDataDir, "data"),
508
+ "--setenv",
509
+ "XDG_CONFIG_HOME",
510
+ join(options.userDataDir, "config"),
511
+ ...readOnlyRoots.flatMap((path) => ["--ro-bind-try", path, path]),
512
+ "--bind",
513
+ options.cwd,
514
+ options.cwd,
515
+ "--bind",
516
+ options.userDataDir,
517
+ options.userDataDir,
518
+ ...(options.extensionsDir === undefined
519
+ ? []
520
+ : ["--bind", options.extensionsDir, options.extensionsDir]),
521
+ "--chdir",
522
+ options.cwd,
523
+ "--",
524
+ codeServerExecutable.command,
525
+ ...codeServerArgs(
526
+ options.port,
527
+ options.cwd,
528
+ options.userDataDir,
529
+ options.extensionsDir,
530
+ ),
531
+ ];
532
+
533
+ return {
534
+ ok: true,
535
+ command: bubblewrapExecutable.command,
536
+ args,
537
+ };
538
+ }
539
+
540
+ function filesystemSandboxUnavailable(): PrepareCodeServerLaunchResult {
541
+ return {
542
+ ok: false,
543
+ reason: "local_editor_filesystem_sandbox_unavailable",
544
+ };
545
+ }
546
+
463
547
  function codeServerSandboxProfile(
464
548
  options: PrepareCodeServerLaunchOptions,
465
549
  codeServerBinDir: string,
@@ -656,7 +740,7 @@ async function startCollaborationSidecar(
656
740
  ]);
657
741
 
658
742
  const child = spawn(
659
- process.execPath,
743
+ nodeRuntimeExecutable(),
660
744
  [
661
745
  join(
662
746
  profile.collaborationServerDir,
@@ -710,6 +794,19 @@ async function startCollaborationSidecar(
710
794
  }
711
795
  }
712
796
 
797
+ export function nodeRuntimeExecutable(
798
+ env: NodeJS.ProcessEnv = process.env,
799
+ execPath = process.execPath,
800
+ ): string {
801
+ const configured = env.LINZUMI_NODE_BIN?.trim();
802
+
803
+ if (configured !== undefined && configured !== "") {
804
+ return configured;
805
+ }
806
+
807
+ return basename(execPath).toLowerCase().includes("bun") ? "node" : execPath;
808
+ }
809
+
713
810
  async function installLocalTarball(
714
811
  archivePath: string,
715
812
  destinationDir: string,
@@ -728,8 +825,9 @@ function codeServerEnv(
728
825
  userDataDir: string,
729
826
  collaboration?: RunningLocalEditorCollaboration | undefined,
730
827
  ): NodeJS.ProcessEnv {
828
+ const { PORT: _port, ...hostEnv } = env;
731
829
  const base = {
732
- ...env,
830
+ ...hostEnv,
733
831
  HOME: userDataDir,
734
832
  XDG_CACHE_HOME: join(userDataDir, "xdg-cache"),
735
833
  XDG_CONFIG_HOME: join(userDataDir, "xdg-config"),
@@ -18,7 +18,7 @@ import {
18
18
  rmSync,
19
19
  writeFileSync,
20
20
  } from "node:fs";
21
- import { homedir, tmpdir } from "node:os";
21
+ import { homedir } from "node:os";
22
22
  import { dirname, join, resolve } from "node:path";
23
23
  import { Readable } from "node:stream";
24
24
  import { pipeline } from "node:stream/promises";
@@ -33,7 +33,14 @@ export type EditorRuntimeManifest = {
33
33
  readonly codeServerVersion: string;
34
34
  readonly codeServerBinPath: string;
35
35
  readonly manifestPath: string;
36
- readonly assets: readonly JsonObject[];
36
+ readonly assets: readonly EditorRuntimeAsset[];
37
+ };
38
+
39
+ export type EditorRuntimeAsset = {
40
+ readonly path: string;
41
+ readonly sha256: string;
42
+ readonly url?: string | undefined;
43
+ readonly contentBase64?: string | undefined;
37
44
  };
38
45
 
39
46
  export type EditorRuntimeStatus =
@@ -186,6 +193,8 @@ export function editorRuntimePlatform(
186
193
  return "darwin-arm64";
187
194
  case "darwin-x64":
188
195
  return "darwin-x64";
196
+ case "linux-x64":
197
+ return "linux-x64";
189
198
  default:
190
199
  return undefined;
191
200
  }
@@ -266,16 +275,15 @@ function normalizeManifest(value: JsonObject):
266
275
  const codeServerVersion = nonEmptyString(value.codeServerVersion);
267
276
  const codeServerBinPath = nonEmptyString(value.codeServerBinPath) ?? "bin/code-server";
268
277
  const manifestPath = nonEmptyString(value.manifestPath) ?? "linzumi-editor-runtime.json";
269
- const assets = Array.isArray(value.assets) && value.assets.every(isJsonObject)
270
- ? value.assets
271
- : [];
278
+ const assets = normalizeRuntimeAssets(value.assets);
272
279
 
273
280
  if (
274
281
  version === undefined ||
275
282
  platform === undefined ||
276
283
  archiveUrl === undefined ||
277
284
  archiveSha256 === undefined ||
278
- codeServerVersion === undefined
285
+ codeServerVersion === undefined ||
286
+ assets === undefined
279
287
  ) {
280
288
  return { ok: false };
281
289
  }
@@ -295,6 +303,38 @@ function normalizeManifest(value: JsonObject):
295
303
  };
296
304
  }
297
305
 
306
+ function normalizeRuntimeAssets(value: unknown): readonly EditorRuntimeAsset[] | undefined {
307
+ if (!Array.isArray(value)) {
308
+ return undefined;
309
+ }
310
+
311
+ const assets: EditorRuntimeAsset[] = [];
312
+
313
+ for (const asset of value) {
314
+ if (!isJsonObject(asset)) {
315
+ return undefined;
316
+ }
317
+
318
+ const path = nonEmptyString(asset.path);
319
+ const sha256 = sha256String(asset.sha256);
320
+ const url = nonEmptyString(asset.url);
321
+ const contentBase64 = nonEmptyString(asset.contentBase64);
322
+
323
+ if (path === undefined || sha256 === undefined) {
324
+ return undefined;
325
+ }
326
+
327
+ assets.push({
328
+ path,
329
+ sha256,
330
+ ...(url === undefined ? {} : { url }),
331
+ ...(contentBase64 === undefined ? {} : { contentBase64 }),
332
+ });
333
+ }
334
+
335
+ return assets;
336
+ }
337
+
298
338
  function installedRuntime(
299
339
  cacheRoot: string,
300
340
  manifest: EditorRuntimeManifest,
@@ -348,7 +388,7 @@ async function installRuntime(args: {
348
388
  > {
349
389
  mkdirSync(args.cacheRoot, { recursive: true });
350
390
 
351
- const tempRoot = mkdtempSync(join(tmpdir(), "linzumi-editor-runtime-install-"));
391
+ const tempRoot = mkdtempSync(join(args.cacheRoot, ".install-"));
352
392
  const archivePath = join(tempRoot, "runtime.tar.gz");
353
393
  const extractRoot = join(tempRoot, "runtime");
354
394
 
@@ -371,33 +411,37 @@ async function installRuntime(args: {
371
411
  return { ok: false, reason: "archive_extract_failed" };
372
412
  }
373
413
 
374
- const manifestPath = join(extractRoot, args.manifest.manifestPath);
375
- const codeServerBin = join(extractRoot, args.manifest.codeServerBinPath);
376
- const assets = verifiedRuntimeAssetPaths(extractRoot, args.manifest);
414
+ const assetsInstalled = await materializeRuntimeAssets({
415
+ kandanUrl: args.kandanUrl,
416
+ manifest: args.manifest,
417
+ runtimeRoot: extractRoot,
418
+ fetchImpl: args.fetchImpl,
419
+ });
377
420
 
378
- if (!existsSync(manifestPath) || !existsSync(codeServerBin) || assets === undefined) {
379
- return { ok: false, reason: "invalid_archive" };
421
+ if (!assetsInstalled) {
422
+ return { ok: false, reason: "install_failed" };
380
423
  }
381
424
 
382
- const installed: unknown = JSON.parse(readFileSync(manifestPath, "utf8"));
425
+ const manifestPath = join(extractRoot, args.manifest.manifestPath);
426
+ const codeServerBin = join(extractRoot, args.manifest.codeServerBinPath);
427
+ const assets = verifiedRuntimeAssetPaths(extractRoot, args.manifest);
383
428
 
384
- if (
385
- !isJsonObject(installed) ||
386
- installed.version !== args.manifest.version ||
387
- installed.platform !== args.manifest.platform ||
388
- (installed.archiveSha256 !== undefined &&
389
- installed.archiveSha256 !== args.manifest.archiveSha256)
390
- ) {
429
+ if (!existsSync(codeServerBin) || assets === undefined) {
391
430
  return { ok: false, reason: "invalid_archive" };
392
431
  }
393
432
 
433
+ mkdirSync(dirname(manifestPath), { recursive: true });
394
434
  writeFileSync(
395
435
  manifestPath,
396
436
  JSON.stringify(
397
437
  {
398
- ...installed,
438
+ version: args.manifest.version,
439
+ platform: args.manifest.platform,
399
440
  archiveSha256: args.manifest.archiveSha256,
400
441
  codeServerVersion: args.manifest.codeServerVersion,
442
+ codeServerBinPath: args.manifest.codeServerBinPath,
443
+ manifestPath: args.manifest.manifestPath,
444
+ assets: args.manifest.assets,
401
445
  },
402
446
  null,
403
447
  2,
@@ -444,6 +488,59 @@ async function installRuntime(args: {
444
488
  }
445
489
  }
446
490
 
491
+ async function materializeRuntimeAssets(args: {
492
+ readonly kandanUrl: string;
493
+ readonly manifest: EditorRuntimeManifest;
494
+ readonly runtimeRoot: string;
495
+ readonly fetchImpl: typeof fetch;
496
+ }): Promise<boolean> {
497
+ for (const asset of args.manifest.assets) {
498
+ const targetPath = join(args.runtimeRoot, asset.path);
499
+
500
+ try {
501
+ const bytes = await runtimeAssetBytes({
502
+ kandanUrl: args.kandanUrl,
503
+ asset,
504
+ fetchImpl: args.fetchImpl,
505
+ });
506
+
507
+ if (bytes === undefined) {
508
+ continue;
509
+ }
510
+
511
+ mkdirSync(dirname(targetPath), { recursive: true });
512
+ writeFileSync(targetPath, bytes);
513
+ } catch (_error) {
514
+ return false;
515
+ }
516
+ }
517
+
518
+ return true;
519
+ }
520
+
521
+ async function runtimeAssetBytes(args: {
522
+ readonly kandanUrl: string;
523
+ readonly asset: EditorRuntimeAsset;
524
+ readonly fetchImpl: typeof fetch;
525
+ }): Promise<Buffer | undefined> {
526
+ if (args.asset.contentBase64 !== undefined) {
527
+ return Buffer.from(args.asset.contentBase64, "base64");
528
+ }
529
+
530
+ if (args.asset.url !== undefined) {
531
+ const url = new URL(args.asset.url, kandanHttpBaseUrl(args.kandanUrl));
532
+ const response = await args.fetchImpl(url);
533
+
534
+ if (response.status !== 200) {
535
+ return undefined;
536
+ }
537
+
538
+ return Buffer.from(await response.arrayBuffer());
539
+ }
540
+
541
+ return undefined;
542
+ }
543
+
447
544
  async function downloadArchive(args: {
448
545
  readonly kandanUrl: string;
449
546
  readonly token: string;
@@ -454,9 +551,12 @@ async function downloadArchive(args: {
454
551
  | { readonly ok: true }
455
552
  | { readonly ok: false; readonly reason: "download_failed" | "checksum_mismatch" }
456
553
  > {
457
- const url = new URL(args.manifest.archiveUrl, kandanHttpBaseUrl(args.kandanUrl));
554
+ const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
555
+ const url = new URL(args.manifest.archiveUrl, kandanBaseUrl);
458
556
  const response = await args.fetchImpl(url, {
459
- headers: { authorization: `Bearer ${args.token}` },
557
+ ...(sameOrigin(url, new URL(kandanBaseUrl))
558
+ ? { headers: { authorization: `Bearer ${args.token}` } }
559
+ : {}),
460
560
  });
461
561
 
462
562
  if (response.status !== 200 || response.body === null) {
@@ -477,6 +577,10 @@ async function downloadArchive(args: {
477
577
  return { ok: true };
478
578
  }
479
579
 
580
+ function sameOrigin(left: URL, right: URL): boolean {
581
+ return left.protocol === right.protocol && left.host === right.host;
582
+ }
583
+
480
584
  function extractTarGz(archivePath: string, destination: string): Promise<boolean> {
481
585
  return new Promise((resolveExtract) => {
482
586
  const child = spawn("tar", ["-xzf", archivePath, "-C", destination], {
@@ -529,10 +633,11 @@ function verifiedRuntimeAssetPaths(
529
633
  "kandan.document-state-telemetry",
530
634
  );
531
635
 
636
+ const codeServerRoot = codeServerRuntimeRoot(manifest.codeServerBinPath);
532
637
  const requiredPaths = [
533
638
  manifest.codeServerBinPath,
534
- "lib/vscode/node_modules/vsda/rust/web/vsda.js",
535
- "lib/vscode/node_modules/vsda/rust/web/vsda_bg.wasm",
639
+ join(codeServerRoot, "lib", "vscode", "node_modules", "vsda", "rust", "web", "vsda.js"),
640
+ join(codeServerRoot, "lib", "vscode", "node_modules", "vsda", "rust", "web", "vsda_bg.wasm"),
536
641
  "kandan/editor_extensions/typefox.open-collaboration-tools.tar.gz",
537
642
  "kandan/editor_servers/open-collaboration-server.tar.gz",
538
643
  "kandan/editor_extensions/kandan.document-state-telemetry/package.json",
@@ -543,13 +648,17 @@ function verifiedRuntimeAssetPaths(
543
648
  for (const relativePath of requiredPaths) {
544
649
  const expectedSha256 = assetChecksums.get(relativePath);
545
650
 
546
- if (expectedSha256 === undefined) {
651
+ if (expectedSha256 === undefined && relativePath !== manifest.codeServerBinPath) {
547
652
  return undefined;
548
653
  }
549
654
 
550
655
  const absolutePath = join(runtimeRoot, relativePath);
551
656
 
552
- if (!existsSync(absolutePath) || fileSha256Sync(absolutePath) !== expectedSha256) {
657
+ if (!existsSync(absolutePath)) {
658
+ return undefined;
659
+ }
660
+
661
+ if (expectedSha256 !== undefined && fileSha256Sync(absolutePath) !== expectedSha256) {
553
662
  return undefined;
554
663
  }
555
664
  }
@@ -565,16 +674,21 @@ function verifiedRuntimeAssetPaths(
565
674
  };
566
675
  }
567
676
 
568
- function manifestAssetChecksums(assets: readonly JsonObject[]): Map<string, string> {
677
+ function codeServerRuntimeRoot(codeServerBinPath: string): string {
678
+ const normalized = codeServerBinPath.replaceAll("\\", "/");
679
+
680
+ return normalized === "bin/code-server"
681
+ ? "."
682
+ : normalized.endsWith("/bin/code-server")
683
+ ? normalized.slice(0, -"/bin/code-server".length) || "."
684
+ : dirname(normalized);
685
+ }
686
+
687
+ function manifestAssetChecksums(assets: readonly EditorRuntimeAsset[]): Map<string, string> {
569
688
  const checksums = new Map<string, string>();
570
689
 
571
690
  for (const asset of assets) {
572
- const path = nonEmptyString(asset.path);
573
- const sha256 = sha256String(asset.sha256);
574
-
575
- if (path !== undefined && sha256 !== undefined) {
576
- checksums.set(path, sha256);
577
- }
691
+ checksums.set(asset.path, asset.sha256);
578
692
  }
579
693
 
580
694
  return checksums;