@linzumi/cli 0.0.14-beta → 0.0.16-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
@@ -19,6 +19,16 @@
19
19
  your agent, your laptop, PRs in minutes
20
20
  ```
21
21
 
22
+ Copy this into your terminal from the repo you want changed:
23
+
24
+ ```bash
25
+ codex -- "Get a working hello-world PR from https://linzumi.com/agents.md"
26
+ ```
27
+
28
+ That's the launch path: Codex opens the agent instructions, signs up,
29
+ asks you for the emailed code, connects Linzumi to this repo, opens the
30
+ browser editor, and posts the PR or review branch.
31
+
22
32
  Today, your AI coding agent has two homes, both bad.
23
33
 
24
34
  It can run **alone in your terminal**, where nobody — your teammate,
@@ -76,12 +86,6 @@ That's it. The rest of this README is detail.
76
86
 
77
87
  ## Agent-first launch path
78
88
 
79
- Copy this into an agent running in the repo you want changed:
80
-
81
- ```text
82
- Get me a working hello-world PR from https://linzumi.com/agents.md
83
- ```
84
-
85
89
  That one prompt is the launch path. The canonical agent instructions are
86
90
  hosted at `https://linzumi.com/agents.md`; `https://linzumi.com/skills.md`
87
91
  stays available for compatibility. The agent will ask for your email,
@@ -103,6 +107,11 @@ workspace/channel scope from the approval flow, trusts only the selected
103
107
  folder by default, and listens only to the approving human unless
104
108
  `--listen-user` is explicitly passed.
105
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
+
106
115
  `linzumi claim` also prints `support_channel_url`. That channel is the
107
116
  shared onboarding room for the approving human, the claimed agent identity,
108
117
  and Linzumi support. Keep repository work in task threads; use the support
@@ -225,7 +234,7 @@ privileges as your shell. Every action is auditable from the thread.
225
234
  ## Pinning a version
226
235
 
227
236
  ```bash
228
- npm install -g @linzumi/cli@0.0.14-beta
237
+ npm install -g @linzumi/cli@0.0.16-beta
229
238
  linzumi --version
230
239
  ```
231
240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.14-beta",
3
+ "version": "0.0.16-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.14-beta\n");
124
+ process.stdout.write("linzumi 0.0.16-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.14-beta\n");
617
+ process.stdout.write("linzumi 0.0.16-beta\n");
618
618
  process.exit(0);
619
619
  }
620
620
 
@@ -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
 
@@ -414,20 +415,18 @@ export function prepareCodeServerLaunch(
414
415
  ): PrepareCodeServerLaunchResult {
415
416
  const platform = options.platform ?? process.platform;
416
417
 
418
+ if (platform === "linux") {
419
+ return prepareLinuxCodeServerLaunch(options);
420
+ }
421
+
417
422
  if (platform !== "darwin") {
418
- return {
419
- ok: false,
420
- reason: "local_editor_filesystem_sandbox_unavailable",
421
- };
423
+ return filesystemSandboxUnavailable();
422
424
  }
423
425
 
424
426
  const sandboxExecBin = options.sandboxExecBin ?? "/usr/bin/sandbox-exec";
425
427
 
426
428
  if (!existsSync(sandboxExecBin)) {
427
- return {
428
- ok: false,
429
- reason: "local_editor_filesystem_sandbox_unavailable",
430
- };
429
+ return filesystemSandboxUnavailable();
431
430
  }
432
431
 
433
432
  const codeServerExecutable = resolveCodeServerExecutable(
@@ -436,10 +435,7 @@ export function prepareCodeServerLaunch(
436
435
  );
437
436
 
438
437
  if (!codeServerExecutable.ok) {
439
- return {
440
- ok: false,
441
- reason: "local_editor_filesystem_sandbox_unavailable",
442
- };
438
+ return filesystemSandboxUnavailable();
443
439
  }
444
440
 
445
441
  return {
@@ -460,6 +456,96 @@ export function prepareCodeServerLaunch(
460
456
  };
461
457
  }
462
458
 
459
+ function prepareLinuxCodeServerLaunch(
460
+ options: PrepareCodeServerLaunchOptions,
461
+ ): PrepareCodeServerLaunchResult {
462
+ const bubblewrapExecutable = resolveCodeServerExecutable(
463
+ options.bubblewrapBin ?? "bwrap",
464
+ options.envPath ?? process.env.PATH ?? "",
465
+ );
466
+
467
+ if (!bubblewrapExecutable.ok) {
468
+ return filesystemSandboxUnavailable();
469
+ }
470
+
471
+ const codeServerExecutable = resolveCodeServerExecutable(
472
+ options.codeServerBin,
473
+ options.envPath ?? process.env.PATH ?? "",
474
+ );
475
+
476
+ if (!codeServerExecutable.ok) {
477
+ return filesystemSandboxUnavailable();
478
+ }
479
+
480
+ const readOnlyRoots = uniquePaths([
481
+ "/usr",
482
+ "/bin",
483
+ "/sbin",
484
+ "/lib",
485
+ "/lib64",
486
+ "/etc",
487
+ "/nix",
488
+ "/opt",
489
+ ...(options.codeServerRuntimeRoot === undefined
490
+ ? []
491
+ : sandboxPathAliases(options.codeServerRuntimeRoot)),
492
+ codeServerExecutable.directory,
493
+ ]);
494
+
495
+ const args = [
496
+ "--die-with-parent",
497
+ "--proc",
498
+ "/proc",
499
+ "--dev-bind",
500
+ "/dev",
501
+ "/dev",
502
+ "--tmpfs",
503
+ "/tmp",
504
+ "--setenv",
505
+ "HOME",
506
+ options.userDataDir,
507
+ "--setenv",
508
+ "XDG_DATA_HOME",
509
+ join(options.userDataDir, "data"),
510
+ "--setenv",
511
+ "XDG_CONFIG_HOME",
512
+ join(options.userDataDir, "config"),
513
+ ...readOnlyRoots.flatMap((path) => ["--ro-bind-try", path, path]),
514
+ "--bind",
515
+ options.cwd,
516
+ options.cwd,
517
+ "--bind",
518
+ options.userDataDir,
519
+ options.userDataDir,
520
+ ...(options.extensionsDir === undefined
521
+ ? []
522
+ : ["--bind", options.extensionsDir, options.extensionsDir]),
523
+ "--chdir",
524
+ options.cwd,
525
+ "--",
526
+ codeServerExecutable.command,
527
+ ...codeServerArgs(
528
+ options.port,
529
+ options.cwd,
530
+ options.userDataDir,
531
+ options.extensionsDir,
532
+ ),
533
+ ];
534
+
535
+ return {
536
+ ok: true,
537
+ command: bubblewrapExecutable.command,
538
+ args,
539
+ };
540
+ }
541
+
542
+ function filesystemSandboxUnavailable(): PrepareCodeServerLaunchResult {
543
+ return {
544
+ ok: false,
545
+ reason: "local_editor_filesystem_sandbox_unavailable",
546
+ };
547
+ }
548
+
463
549
  function codeServerSandboxProfile(
464
550
  options: PrepareCodeServerLaunchOptions,
465
551
  codeServerBinDir: string,
@@ -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,
@@ -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;