larkway 0.3.64 → 0.3.66

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
@@ -8,7 +8,7 @@
8
8
 
9
9
  You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
10
10
 
11
- **Current release: v0.3.64**
11
+ **Current release: v0.3.66**
12
12
 
13
13
  ---
14
14
 
@@ -141,6 +141,12 @@ not enforcement.
141
141
  - Secrets in `~/.larkway/.env` are shared across all bots on the bridge and
142
142
  visible to any agent that can read your home directory — don't mix a
143
143
  low-trust open bot and high-value credentials on the same host.
144
+ - By default all bots share the host's global lark-cli config dir — including
145
+ any personal `lark-cli auth login` you did there (calendar/mail/drive).
146
+ Set `lark_cli_isolated: true` in a bot's yaml to give it a private config
147
+ dir (`~/.larkway/<botId>/lark-cli/`) holding only its own app profile:
148
+ bot-only identity by default, personal access only if you explicitly
149
+ authorize inside that dir.
144
150
 
145
151
  ---
146
152
 
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  你在飞书话题里 @ bot,它在你的机器上运行——读真实代码库、执行命令、开 MR——把结果贴回飞书。你定义 agent 知道什么、能做什么。Larkway 只负责传递消息。
10
10
 
11
- **当前版本:v0.3.64**
11
+ **当前版本:v0.3.66**
12
12
 
13
13
  ---
14
14
 
@@ -115,6 +115,8 @@ Larkway 不注入 `ANTHROPIC_API_KEY` 或任何其他 API key。子进程继承
115
115
 
116
116
  密钥只存在本机 `~/.larkway/.env`(权限 0600)。配置和 memory 不含密钥。
117
117
 
118
+ 默认所有 bot 共享宿主机的全局 lark-cli 配置目录——包括你在里面做过的个人 `lark-cli auth login`(日历/邮箱/云盘)。在 bot 的 yaml 里设 `lark_cli_isolated: true` 可给它一个私有配置目录(`~/.larkway/<botId>/lark-cli/`),里面只有它自己的应用 profile:默认 bot-only 身份,只有你显式在该目录内授权,agent 才能获得个人资源访问。
119
+
118
120
  ---
119
121
 
120
122
  ## 功能
package/dist/cli/index.js CHANGED
@@ -53,6 +53,7 @@ __export(paths_exports, {
53
53
  resolveAgentWorkspacePathFromHome: () => resolveAgentWorkspacePathFromHome,
54
54
  resolveAgentWorkspaceReposDir: () => resolveAgentWorkspaceReposDir,
55
55
  resolveAgentWorkspaceSessionsDir: () => resolveAgentWorkspaceSessionsDir,
56
+ resolveBotLarkCliDir: () => resolveBotLarkCliDir,
56
57
  resolveCandidateAlertsPath: () => resolveCandidateAlertsPath,
57
58
  resolveKnowledgeDir: () => resolveKnowledgeDir,
58
59
  resolveLarkwayDir: () => resolveLarkwayDir,
@@ -70,6 +71,9 @@ function larkwayHome() {
70
71
  if (env && env.trim() !== "") return resolve(env);
71
72
  return join(homedir(), ".larkway");
72
73
  }
74
+ function resolveBotLarkCliDir(botId) {
75
+ return join(resolveLarkwayDir(botId), "lark-cli");
76
+ }
73
77
  function resolveLarkwayDir(botId) {
74
78
  if (botId === void 0 || botId === LEGACY_BOT_ID) {
75
79
  return larkwayHome();
@@ -111848,6 +111852,9 @@ import { execFile as execFileCallback } from "node:child_process";
111848
111852
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4, rename as rename3, unlink as unlink4 } from "node:fs/promises";
111849
111853
  import path9 from "node:path";
111850
111854
  import { promisify as promisify4 } from "node:util";
111855
+ function larkCliEnvFor(configDir) {
111856
+ return configDir ? { ...process.env, LARKSUITE_CLI_CONFIG_DIR: configDir } : void 0;
111857
+ }
111851
111858
  function isChatAccessGoneError(e) {
111852
111859
  const parts = [];
111853
111860
  if (e instanceof Error) parts.push(e.message);
@@ -112783,7 +112790,10 @@ var init_channelClient = __esm({
112783
112790
  for (let attempt = 1; attempt <= GAP_FILL_MAX_ATTEMPTS; attempt++) {
112784
112791
  if (this.closed) throw lastErr ?? new Error("closed");
112785
112792
  try {
112786
- return await execFile4(larkCli, args);
112793
+ {
112794
+ const isoEnv = larkCliEnvFor(this.opts.larkCliConfigDir);
112795
+ return isoEnv ? await execFile4(larkCli, args, { env: isoEnv }) : await execFile4(larkCli, args);
112796
+ }
112787
112797
  } catch (e) {
112788
112798
  lastErr = e;
112789
112799
  if (isChatAccessGoneError(e)) throw e;
@@ -112945,7 +112955,8 @@ var init_channelClient = __esm({
112945
112955
  ...profileArgs
112946
112956
  ];
112947
112957
  if (pageToken) args.push("--page-token", pageToken);
112948
- const { stdout: stdout2 } = await execFile4(larkCli, args);
112958
+ const isoEnv = larkCliEnvFor(this.opts.larkCliConfigDir);
112959
+ const { stdout: stdout2 } = isoEnv ? await execFile4(larkCli, args, { env: isoEnv }) : await execFile4(larkCli, args);
112949
112960
  const parsed = JSON.parse(stdout2);
112950
112961
  const chats = parseLarkCliMessages(stdout2) ?? [];
112951
112962
  fetched += chats.length;
@@ -126009,6 +126020,17 @@ var BotConfigSchema = external_exports.object({
126009
126020
  * V1 single-bot path doesn't need this (uses default profile naturally).
126010
126021
  */
126011
126022
  lark_cli_profile: external_exports.string().min(1).optional(),
126023
+ /**
126024
+ * BL-50: per-bot lark-cli identity isolation. When true, every lark-cli
126025
+ * invocation for this bot (agent subprocess, gap-fill, roster, health scan)
126026
+ * runs with LARKSUITE_CLI_CONFIG_DIR pointed at ~/.larkway/<botId>/lark-cli/
126027
+ * — a private config dir holding ONLY this bot's app profile. The
126028
+ * maintainer's personal lark-cli login (calendar/mail/drive access) in the
126029
+ * shared global dir becomes invisible to this bot's agent ("bot-only" by
126030
+ * default; grant user identity by running `lark-cli auth login` with the
126031
+ * same LARKSUITE_CLI_CONFIG_DIR). Default false = shared dir (pre-BL-50).
126032
+ */
126033
+ lark_cli_isolated: external_exports.boolean().optional(),
126012
126034
  /**
126013
126035
  * Env-var *name* (not value) that holds this bot's Git PAT.
126014
126036
  * Read from process.env at startup and injected as GITLAB_TOKEN into the
@@ -130180,6 +130202,7 @@ import { spawn as spawn2, execFile as execFile7 } from "node:child_process";
130180
130202
  import { openSync, constants as fsConstants2 } from "node:fs";
130181
130203
  import { readFile as readFile10, writeFile as writeFile9, unlink as unlink6, mkdir as mkdir8, readdir as readdir5, access as access8 } from "node:fs/promises";
130182
130204
  import { promisify as promisify7 } from "node:util";
130205
+ import { fileURLToPath } from "node:url";
130183
130206
  import path17 from "node:path";
130184
130207
  import process5 from "node:process";
130185
130208
  import { stat as stat4 } from "node:fs/promises";
@@ -130555,7 +130578,7 @@ function bridgePidPath(larkwayDir) {
130555
130578
  return path17.join(larkwayDir, "bridge.pid");
130556
130579
  }
130557
130580
  function resolveRepoRoot() {
130558
- const here = new URL(import.meta.url).pathname;
130581
+ const here = fileURLToPath(import.meta.url);
130559
130582
  return path17.resolve(path17.dirname(here), "..", "..");
130560
130583
  }
130561
130584
  async function resolveDistMain() {
@@ -131532,7 +131555,7 @@ import { randomBytes } from "node:crypto";
131532
131555
  import { existsSync } from "node:fs";
131533
131556
  import { readFile as readFile14, stat as stat6 } from "node:fs/promises";
131534
131557
  import path27 from "node:path";
131535
- import { fileURLToPath as fileURLToPath2 } from "node:url";
131558
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
131536
131559
 
131537
131560
  // src/web/api.ts
131538
131561
  import { readdir as readdir6, readFile as readFile13, stat as stat5 } from "node:fs/promises";
@@ -131694,10 +131717,10 @@ async function summarizeMemoryMetrics(filePath = resolveMemoryMetricsPath(), sin
131694
131717
 
131695
131718
  // src/version.ts
131696
131719
  import { readFileSync } from "node:fs";
131697
- import { fileURLToPath } from "node:url";
131720
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
131698
131721
  import path23 from "node:path";
131699
131722
  function resolveLarkwayVersion(importMetaUrl, fallback = "unknown") {
131700
- const here = path23.dirname(fileURLToPath(importMetaUrl));
131723
+ const here = path23.dirname(fileURLToPath2(importMetaUrl));
131701
131724
  const candidates = [
131702
131725
  path23.join(here, "..", "package.json"),
131703
131726
  path23.join(here, "..", "..", "package.json")
@@ -132395,7 +132418,12 @@ async function runHealthScan(bot, hostConfigStore) {
132395
132418
  ["--profile", profile, "auth", "status", "--json"],
132396
132419
  // commandProbeEnv:补齐 ~/.local/bin 等用户级安装目录(mini 实测:精简
132397
132420
  // PATH 的 UI 进程探测不到工具,与登录态探测自相矛盾)
132398
- { timeout: HEALTH_SCAN_PROBE_TIMEOUT_MS, maxBuffer: 1024 * 1024, env: commandProbeEnv() }
132421
+ {
132422
+ timeout: HEALTH_SCAN_PROBE_TIMEOUT_MS,
132423
+ maxBuffer: 1024 * 1024,
132424
+ // BL-50: probe an isolated bot inside its private config dir.
132425
+ env: bot.lark_cli_isolated ? { ...commandProbeEnv(), LARKSUITE_CLI_CONFIG_DIR: resolveBotLarkCliDir(bot.id) } : commandProbeEnv()
132426
+ }
132399
132427
  );
132400
132428
  const payload = healthScanParseJson(stdout2);
132401
132429
  const botIdentity = payload?.identities?.bot;
@@ -133462,7 +133490,7 @@ function splitPath(p) {
133462
133490
 
133463
133491
  // src/web/server.ts
133464
133492
  var HOST = "127.0.0.1";
133465
- var __dirname2 = path27.dirname(fileURLToPath2(import.meta.url));
133493
+ var __dirname2 = path27.dirname(fileURLToPath3(import.meta.url));
133466
133494
  var PUBLIC_DIR = resolvePublicDir();
133467
133495
  function resolvePublicDir() {
133468
133496
  const candidates = [
package/dist/main.js CHANGED
@@ -115576,6 +115576,9 @@ function larkwayHome() {
115576
115576
  if (env && env.trim() !== "") return resolve(env);
115577
115577
  return join(homedir(), ".larkway");
115578
115578
  }
115579
+ function resolveBotLarkCliDir(botId) {
115580
+ return join(resolveLarkwayDir(botId), "lark-cli");
115581
+ }
115579
115582
  function resolveLarkwayDir(botId) {
115580
115583
  if (botId === void 0 || botId === LEGACY_BOT_ID) {
115581
115584
  return larkwayHome();
@@ -116486,6 +116489,9 @@ var ChannelMessageLookupClient = class {
116486
116489
 
116487
116490
  // src/lark/channelClient.ts
116488
116491
  var execFile = promisify(execFileCallback);
116492
+ function larkCliEnvFor(configDir) {
116493
+ return configDir ? { ...process.env, LARKSUITE_CLI_CONFIG_DIR: configDir } : void 0;
116494
+ }
116489
116495
  var LEARNED_CHATS_LIMIT = 100;
116490
116496
  var SEEN_MESSAGES_LIMIT = 1e3;
116491
116497
  var MAX_MESSAGE_ATTEMPTS = 5;
@@ -117409,7 +117415,10 @@ var ChannelClient = class {
117409
117415
  for (let attempt = 1; attempt <= GAP_FILL_MAX_ATTEMPTS; attempt++) {
117410
117416
  if (this.closed) throw lastErr ?? new Error("closed");
117411
117417
  try {
117412
- return await execFile(larkCli, args);
117418
+ {
117419
+ const isoEnv = larkCliEnvFor(this.opts.larkCliConfigDir);
117420
+ return isoEnv ? await execFile(larkCli, args, { env: isoEnv }) : await execFile(larkCli, args);
117421
+ }
117413
117422
  } catch (e) {
117414
117423
  lastErr = e;
117415
117424
  if (isChatAccessGoneError(e)) throw e;
@@ -117571,7 +117580,8 @@ var ChannelClient = class {
117571
117580
  ...profileArgs
117572
117581
  ];
117573
117582
  if (pageToken) args.push("--page-token", pageToken);
117574
- const { stdout } = await execFile(larkCli, args);
117583
+ const isoEnv = larkCliEnvFor(this.opts.larkCliConfigDir);
117584
+ const { stdout } = isoEnv ? await execFile(larkCli, args, { env: isoEnv }) : await execFile(larkCli, args);
117575
117585
  const parsed = JSON.parse(stdout);
117576
117586
  const chats = parseLarkCliMessages(stdout) ?? [];
117577
117587
  fetched += chats.length;
@@ -118952,6 +118962,7 @@ function deriveTriggerFacts(parsed, isNewThread, larkCliProfile) {
118952
118962
  var ANSWER_BEGIN_MARKER = "LARKWAY_ANSWER_BEGIN";
118953
118963
  var ANSWER_END_MARKER = "LARKWAY_ANSWER_END";
118954
118964
  var STREAM_HOLD_CHARS = ANSWER_END_MARKER.length + 2;
118965
+ var WAITING_CATCH_UP_MAX_CHARS = 16 * 1024;
118955
118966
  function stripLeadingNewline(text) {
118956
118967
  return text.replace(/^\r?\n/, "");
118957
118968
  }
@@ -118989,6 +119000,7 @@ var AnswerChannelExtractor = class {
118989
119000
  buffer = "";
118990
119001
  visibleText = "";
118991
119002
  lastSnapshotText = "";
119003
+ lastWaitingCatchUpText = "";
118992
119004
  ingestDelta(text, raw) {
118993
119005
  if (!text || this.mode === "closed") return [];
118994
119006
  this.buffer += text;
@@ -119018,15 +119030,45 @@ var AnswerChannelExtractor = class {
119018
119030
  if (this.lastSnapshotText && text.startsWith(this.lastSnapshotText)) {
119019
119031
  const delta = text.slice(this.lastSnapshotText.length);
119020
119032
  this.lastSnapshotText = text;
119021
- return delta ? this.ingestDelta(delta, raw) : [];
119033
+ const events = delta ? this.ingestDelta(delta, raw) : [];
119034
+ return this.withWaitingCatchUp(events, text, raw);
119022
119035
  }
119023
119036
  if (this.lastSnapshotText === "") {
119024
119037
  this.lastSnapshotText = text;
119025
- return this.ingestDelta(text, raw);
119038
+ return this.withWaitingCatchUp(this.ingestDelta(text, raw), text, raw);
119026
119039
  }
119027
119040
  this.lastSnapshotText = text;
119028
119041
  return this.ingestSnapshot(text, raw);
119029
119042
  }
119043
+ /**
119044
+ * Markerless catch-up for the growing-snapshot (delta-routed) path.
119045
+ *
119046
+ * The two ingestDelta-backed branches above emit NOTHING while the
119047
+ * extractor is still waiting for ANSWER_BEGIN — drain() just trims the
119048
+ * waiting buffer. For a turn whose entire reply carries no marker at all
119049
+ * (the agent forgot to write LARKWAY_ANSWER_BEGIN), the claude streaming
119050
+ * path therefore produced ZERO events, so the bridge's untrusted-text
119051
+ * rescue (handler.ts lastInternalText) could never fire and the user got
119052
+ * the "没有产出正文" error card while the full answer existed in the
119053
+ * transcript. The plain ingestSnapshot path already emits such text as
119054
+ * internal_text (splitAnswerChannelText's no-marker branch) — this aligns
119055
+ * the growing-snapshot path with that behavior: after a snapshot has been
119056
+ * delta-routed, if the WHOLE snapshot still contains no BEGIN marker,
119057
+ * re-emit it (bounded, deduped against the previous catch-up) as
119058
+ * internal_text.
119059
+ *
119060
+ * Marker semantics are untouched: the moment BEGIN appears anywhere in the
119061
+ * snapshot, ingestDelta's drain() has already transitioned out of waiting
119062
+ * mode (emitting the before-text exactly once), so the catch-up never
119063
+ * fires for marker-bearing turns.
119064
+ */
119065
+ withWaitingCatchUp(events, snapshotText, raw) {
119066
+ if (this.mode !== "waiting") return events;
119067
+ const bounded = snapshotText.length > WAITING_CATCH_UP_MAX_CHARS ? snapshotText.slice(snapshotText.length - WAITING_CATCH_UP_MAX_CHARS) : snapshotText;
119068
+ if (!bounded.trim() || bounded === this.lastWaitingCatchUpText) return events;
119069
+ this.lastWaitingCatchUpText = bounded;
119070
+ return [...events, { type: "internal_text", text: bounded, raw }];
119071
+ }
119030
119072
  drain(raw) {
119031
119073
  const events = [];
119032
119074
  if (this.mode === "waiting") {
@@ -119687,7 +119729,7 @@ async function resolveChatBotRoster(chatId, opts = {}) {
119687
119729
  const cli = opts.larkCliPath ?? "lark-cli";
119688
119730
  const args = ["im", "chat.members", "bots", "--chat-id", chatId, "--as", "bot"];
119689
119731
  if (opts.profile) args.push("--profile", opts.profile);
119690
- const exec = opts.exec ?? defaultExec(opts.timeoutMs ?? 1e4);
119732
+ const exec = opts.exec ?? defaultExec(opts.timeoutMs ?? 1e4, opts.larkCliConfigDir);
119691
119733
  try {
119692
119734
  const stdout = await exec(cli, args);
119693
119735
  const roster = parseBotRoster(stdout);
@@ -119696,9 +119738,10 @@ async function resolveChatBotRoster(chatId, opts = {}) {
119696
119738
  return null;
119697
119739
  }
119698
119740
  }
119699
- function defaultExec(timeoutMs) {
119741
+ function defaultExec(timeoutMs, larkCliConfigDir) {
119742
+ const env = larkCliConfigDir ? { ...process.env, LARKSUITE_CLI_CONFIG_DIR: larkCliConfigDir } : void 0;
119700
119743
  return (cmd, args) => new Promise((resolve2, reject) => {
119701
- execFile2(cmd, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout) => {
119744
+ execFile2(cmd, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024, ...env ? { env } : {} }, (err, stdout) => {
119702
119745
  if (err) reject(err);
119703
119746
  else resolve2(stdout);
119704
119747
  });
@@ -124194,6 +124237,8 @@ var BridgeHandler = class {
124194
124237
  // V2: inject per-bot git identity; absent in V1 → runner.ts uses "larkway-bot" fallback
124195
124238
  botGitIdentity: this.deps.botConfig?.git_identity,
124196
124239
  gitlabToken: this.deps.gitlabToken,
124240
+ // BL-50: isolated bots get a private lark-cli config dir.
124241
+ ...this.deps.botConfig?.lark_cli_isolated && this.deps.botConfig.id ? { larkCliConfigDir: resolveBotLarkCliDir(this.deps.botConfig.id) } : {},
124197
124242
  model: this.deps.botConfig?.model,
124198
124243
  effort: this.deps.botConfig?.effort,
124199
124244
  onPerfMarker: (marker, atMs) => {
@@ -128438,6 +128483,17 @@ var BotConfigSchema = external_exports.object({
128438
128483
  * V1 single-bot path doesn't need this (uses default profile naturally).
128439
128484
  */
128440
128485
  lark_cli_profile: external_exports.string().min(1).optional(),
128486
+ /**
128487
+ * BL-50: per-bot lark-cli identity isolation. When true, every lark-cli
128488
+ * invocation for this bot (agent subprocess, gap-fill, roster, health scan)
128489
+ * runs with LARKSUITE_CLI_CONFIG_DIR pointed at ~/.larkway/<botId>/lark-cli/
128490
+ * — a private config dir holding ONLY this bot's app profile. The
128491
+ * maintainer's personal lark-cli login (calendar/mail/drive access) in the
128492
+ * shared global dir becomes invisible to this bot's agent ("bot-only" by
128493
+ * default; grant user identity by running `lark-cli auth login` with the
128494
+ * same LARKSUITE_CLI_CONFIG_DIR). Default false = shared dir (pre-BL-50).
128495
+ */
128496
+ lark_cli_isolated: external_exports.boolean().optional(),
128441
128497
  /**
128442
128498
  * Env-var *name* (not value) that holds this bot's Git PAT.
128443
128499
  * Read from process.env at startup and injected as GITLAB_TOKEN into the
@@ -129669,9 +129725,12 @@ import { writeFile as writeFile3, unlink as unlink3, mkdir as mkdir3 } from "nod
129669
129725
  import { join as join2 } from "node:path";
129670
129726
  import { createInterface } from "node:readline";
129671
129727
  var SIGKILL_GRACE_MS = 5e3;
129672
- function buildEnv(botGitIdentity, gitlabToken) {
129728
+ function buildEnv(botGitIdentity, gitlabToken, larkCliConfigDir) {
129673
129729
  const env = { ...process.env };
129674
129730
  delete env["ANTHROPIC_API_KEY"];
129731
+ if (larkCliConfigDir !== void 0) {
129732
+ env["LARKSUITE_CLI_CONFIG_DIR"] = larkCliConfigDir;
129733
+ }
129675
129734
  if (botGitIdentity) {
129676
129735
  env["GIT_AUTHOR_NAME"] = botGitIdentity.name;
129677
129736
  env["GIT_AUTHOR_EMAIL"] = botGitIdentity.email;
@@ -129831,7 +129890,7 @@ function* parseLinesMulti(line, answerExtractor = new AnswerChannelExtractor())
129831
129890
  function runClaude(opts) {
129832
129891
  const timeoutMs = opts.timeoutMs ?? 15 * 60 * 1e3;
129833
129892
  const [bin, args] = buildCommand(opts);
129834
- const env = buildEnv(opts.botGitIdentity, opts.gitlabToken);
129893
+ const env = buildEnv(opts.botGitIdentity, opts.gitlabToken, opts.larkCliConfigDir);
129835
129894
  const markPerf = createPerfMarker(opts.onPerfMarker);
129836
129895
  const child = spawnPipedOutput(bin, args, {
129837
129896
  env,
@@ -130078,6 +130137,7 @@ var ClaudeProcessPool = class {
130078
130137
  #botId;
130079
130138
  #botGitIdentity;
130080
130139
  #gitlabToken;
130140
+ #larkCliConfigDir;
130081
130141
  #idleMs;
130082
130142
  #maxProcesses;
130083
130143
  #pidListFilePath;
@@ -130120,6 +130180,7 @@ var ClaudeProcessPool = class {
130120
130180
  this.#botId = opts.botId;
130121
130181
  this.#botGitIdentity = opts.botGitIdentity;
130122
130182
  this.#gitlabToken = opts.gitlabToken;
130183
+ this.#larkCliConfigDir = opts.larkCliConfigDir;
130123
130184
  this.#idleMs = opts.idleMs ?? DEFAULT_WARM_PROCESS_IDLE_MS;
130124
130185
  this.#maxProcesses = opts.maxProcesses ?? DEFAULT_MAX_PROCESSES;
130125
130186
  this.#pidListFilePath = opts.pidListFilePath;
@@ -130524,7 +130585,7 @@ var ClaudeProcessPool = class {
130524
130585
  }
130525
130586
  #spawnEntry(key, threadId, opts, flags) {
130526
130587
  const [bin, args] = buildWarmCommand(opts);
130527
- const env = buildEnv(this.#botGitIdentity, this.#gitlabToken);
130588
+ const env = buildEnv(this.#botGitIdentity, this.#gitlabToken, this.#larkCliConfigDir);
130528
130589
  const child = spawnPiped(bin, args, {
130529
130590
  env,
130530
130591
  ...opts.cwd != null ? { cwd: opts.cwd } : {}
@@ -130872,10 +130933,13 @@ function productizeCodexFailure(stderr) {
130872
130933
  }
130873
130934
  return void 0;
130874
130935
  }
130875
- function buildCodexEnv(botGitIdentity, gitlabToken) {
130936
+ function buildCodexEnv(botGitIdentity, gitlabToken, larkCliConfigDir) {
130876
130937
  const env = { ...process.env };
130877
130938
  delete env["OPENAI_API_KEY"];
130878
130939
  delete env["ANTHROPIC_API_KEY"];
130940
+ if (larkCliConfigDir !== void 0) {
130941
+ env["LARKSUITE_CLI_CONFIG_DIR"] = larkCliConfigDir;
130942
+ }
130879
130943
  if (botGitIdentity) {
130880
130944
  env["GIT_AUTHOR_NAME"] = botGitIdentity.name;
130881
130945
  env["GIT_AUTHOR_EMAIL"] = botGitIdentity.email;
@@ -131002,7 +131066,7 @@ function extractThreadIdFromThreadResponse(obj) {
131002
131066
  function runCodex(opts, codexBinPath = "codex") {
131003
131067
  const timeoutMs = opts.timeoutMs ?? 15 * 60 * 1e3;
131004
131068
  const [bin, args] = buildCodexCommand(opts, codexBinPath);
131005
- const env = buildCodexEnv(opts.botGitIdentity, opts.gitlabToken);
131069
+ const env = buildCodexEnv(opts.botGitIdentity, opts.gitlabToken, opts.larkCliConfigDir);
131006
131070
  const mode = opts.permissionMode ?? "acceptEdits";
131007
131071
  const requestById = /* @__PURE__ */ new Map();
131008
131072
  let nextRequestId = 1;
@@ -131262,6 +131326,7 @@ var CodexProcessPool = class {
131262
131326
  #codexBinPath;
131263
131327
  #botGitIdentity;
131264
131328
  #gitlabToken;
131329
+ #larkCliConfigDir;
131265
131330
  #idleMs;
131266
131331
  #pidFilePath;
131267
131332
  #child;
@@ -131290,6 +131355,7 @@ var CodexProcessPool = class {
131290
131355
  this.#codexBinPath = opts.codexBinPath;
131291
131356
  this.#botGitIdentity = opts.botGitIdentity;
131292
131357
  this.#gitlabToken = opts.gitlabToken;
131358
+ this.#larkCliConfigDir = opts.larkCliConfigDir;
131293
131359
  this.#idleMs = opts.idleMs ?? DEFAULT_WARM_PROCESS_IDLE_MS2;
131294
131360
  this.#pidFilePath = opts.pidFilePath;
131295
131361
  }
@@ -131521,7 +131587,7 @@ var CodexProcessPool = class {
131521
131587
  // -- process lifecycle -------------------------------------------------------
131522
131588
  #spawnChild() {
131523
131589
  const [bin, args] = buildCodexCommand({ prompt: "" }, this.#codexBinPath);
131524
- const env = buildCodexEnv(this.#botGitIdentity, this.#gitlabToken);
131590
+ const env = buildCodexEnv(this.#botGitIdentity, this.#gitlabToken, this.#larkCliConfigDir);
131525
131591
  const child = spawnPiped(bin, args, { env });
131526
131592
  this.#child = child;
131527
131593
  this.#pending.clear();
@@ -131807,7 +131873,7 @@ async function reapOrphanedWarmProcess(pidFilePath) {
131807
131873
  }
131808
131874
 
131809
131875
  // src/lark/profileBootstrap.ts
131810
- function ensureLarkCliProfile(botId, profileName, appId, appSecret, _spawnSync = spawnProcessSync, _console = console) {
131876
+ function ensureLarkCliProfile(botId, profileName, appId, appSecret, _spawnSync = spawnProcessSync, _console = console, opts) {
131811
131877
  _console.log(
131812
131878
  `[larkway] bot "${botId}": provisioning lark-cli profile "${profileName}" for app ${appId.slice(0, 8)}\u2026`
131813
131879
  );
@@ -131818,7 +131884,9 @@ function ensureLarkCliProfile(botId, profileName, appId, appSecret, _spawnSync =
131818
131884
  {
131819
131885
  input: appSecret,
131820
131886
  encoding: "utf-8",
131821
- timeout: 1e4
131887
+ timeout: 1e4,
131888
+ // BL-50: provision inside the bot's private config dir when isolated.
131889
+ ...opts?.configDir ? { env: { ...process.env, LARKSUITE_CLI_CONFIG_DIR: opts.configDir } } : {}
131822
131890
  }
131823
131891
  );
131824
131892
  if (initResult.status === 0) {
@@ -134578,7 +134646,14 @@ async function runV2Mode({
134578
134646
  );
134579
134647
  }
134580
134648
  const larkCliProfile = deriveLarkCliProfile(bot.lark_cli_profile, bot.app_id);
134581
- ensureLarkCliProfile(bot.id, larkCliProfile, bot.app_id, appSecret);
134649
+ const larkCliConfigDir = bot.lark_cli_isolated ? resolveBotLarkCliDir(bot.id) : void 0;
134650
+ if (larkCliConfigDir) {
134651
+ mkdirSync(larkCliConfigDir, { recursive: true });
134652
+ console.log(`[larkway] bot "${bot.id}": lark-cli identity isolation ON (${larkCliConfigDir})`);
134653
+ }
134654
+ ensureLarkCliProfile(bot.id, larkCliProfile, bot.app_id, appSecret, void 0, void 0, {
134655
+ ...larkCliConfigDir ? { configDir: larkCliConfigDir } : {}
134656
+ });
134582
134657
  const botDir = resolveLarkwayDir(bot.id);
134583
134658
  const sharedReposDir = path25.join(larkwayHome(), "repos");
134584
134659
  const worktreesDir = path25.join(botDir, "worktrees");
@@ -134605,6 +134680,7 @@ async function runV2Mode({
134605
134680
  appId: bot.app_id,
134606
134681
  appSecret,
134607
134682
  larkCliProfile,
134683
+ ...larkCliConfigDir ? { larkCliConfigDir } : {},
134608
134684
  larkwayDir: larkwayHome()
134609
134685
  });
134610
134686
  client.seedTrackedChats(
@@ -134868,6 +134944,7 @@ async function runV2Mode({
134868
134944
  codexPool = new CodexProcessPool({
134869
134945
  botGitIdentity: bot.git_identity,
134870
134946
  gitlabToken: effectiveGitlabToken,
134947
+ ...larkCliConfigDir ? { larkCliConfigDir } : {},
134871
134948
  idleMs: bot.warmProcessIdleMs,
134872
134949
  pidFilePath
134873
134950
  });
@@ -134885,6 +134962,7 @@ async function runV2Mode({
134885
134962
  botId: bot.id,
134886
134963
  botGitIdentity: bot.git_identity,
134887
134964
  gitlabToken: effectiveGitlabToken,
134965
+ ...larkCliConfigDir ? { larkCliConfigDir } : {},
134888
134966
  idleMs: bot.warmProcessIdleMs,
134889
134967
  maxProcesses: bot.warmProcessMaxProcesses,
134890
134968
  pidListFilePath
@@ -134938,7 +135016,8 @@ async function runV2Mode({
134938
135016
  p2pStickyIdleMs: bot.p2pStickyIdleMs,
134939
135017
  owner_open_id: bot.owner_open_id,
134940
135018
  cot: bot.cot,
134941
- cotSurface: bot.cotSurface
135019
+ cotSurface: bot.cotSurface,
135020
+ lark_cli_isolated: bot.lark_cli_isolated
134942
135021
  },
134943
135022
  cardKitClient,
134944
135023
  cotClient,
@@ -134953,7 +135032,7 @@ async function runV2Mode({
134953
135032
  larkCliProfile,
134954
135033
  // PRB-6/§11.3: resolve peer @ targets to same-app-scope open_ids from the
134955
135034
  // live chat roster (per-chat cached), only when this bot actually has peers.
134956
- resolveLiveRoster: resolvedPeers.length > 0 ? createCachedRosterResolver({ profile: larkCliProfile }) : void 0,
135035
+ resolveLiveRoster: resolvedPeers.length > 0 ? createCachedRosterResolver({ profile: larkCliProfile, ...larkCliConfigDir ? { larkCliConfigDir } : {} }) : void 0,
134957
135036
  runtimeRequirements: runtimeRequirementsForBots([bot]),
134958
135037
  recordRuntimeEvent: async (patch) => {
134959
135038
  await upsertRuntimeEvent(larkwayHome(), bot.id, patch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.64",
3
+ "version": "0.3.66",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",