Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions docs/design/2026-07-30-api-only-core-only-bot-mode.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions docs/file-sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ worker spawnCli

→ **所有飞书密钥全程不进沙盒**。

## no-transport 会话(apiOnly / HTTP virtual)跟随本地配置

no-transport 会话(core-only `apiOnly` bot、或 `http_async_*`/`http_wait_*` HTTP virtual 会话)**不被自动强制文件隔离**。它们的磁盘可读写范围和普通聊天会话一样,只由 bot 自己的 `sandbox`/`readIsolation` 配置决定:没配 → 不隔离(以同一 OS 用户身份对宿主文件有完整**读写**权,能读宿主 `bots.json`、也能改写宿主配置);配了 → 照常隔离。

> 早先版本曾把「会话没有飞书 transport 通道」当作强制隔离条件(no-transport ⇒ 一律关进沙盒)。现已去掉这条写死的强制,改为跟随 owner 自己的配置——单 bot 部署 / 载荷可信时不再被无谓束缚。**多 bot 同机**、且担心某个半受信任的 no-transport 会话横向读到**兄弟 bot 的凭证**(`bots.json` 里各 bot 的 app secret)时,需 owner **显式**给该 bot 开 `sandbox`(或 `readIsolation` / 全局 `BOTMUX_SANDBOX=1`)。不开沙盒时 agent 拿到的是同一 OS 用户的宿主读写能力:不仅能读出各 bot secret,还能据此直接调 Lark API、或改写宿主上的任意配置——这正是「载荷可信」这一前提要承担的信任面。

两条与文件沙盒正交、不受此放宽影响的边界仍在:① **本 bot 自己的 transport secret 不进 CLI 进程 env**(gated on transport 能力)——这只关闭 **Botmux 内建的 transport 调用链**(本 bot 的 send 路径),**不构成恶意代码下的凭证隔离**:不开沙盒时 agent 仍能从磁盘 `bots.json` 读出 secret 自行调 Lark;② enrolled 设备上的 **device-credential 强制隔离**独立生效,与本开关无关。

## 落盘(改动去向)

fs-policy 模型下 agent 在 **readWrite 白名单区(含 workingDir)直接写宿主真实文件**——改动即时落盘,不再是「副本 + 补丁交回」。沙盒的作用是把可写面收敛到白名单:项目目录可写、认证目录可写,白名单之外(别的项目、别的会话、`~/.ssh`/`~/.aws`、`bots.json`、各类密钥)一律读不到写不了。
Expand Down
26 changes: 25 additions & 1 deletion src/adapters/backend/herdr-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,18 @@ export class HerdrBackend implements SessionBackend {
cliPid?: number;
cliCwd?: string;

/** Default managed agent name for a Botmux-launched CLI (the single source of
* truth shared by the constructor default and the selector's agent-precise
* reattach probe). */
static defaultAgentName(): string {
return 'botmux';
}

constructor(
readonly sessionName: string,
private readonly opts: HerdrBackendOptions = {},
) {
this.agentName = opts.agentName ?? 'botmux';
this.agentName = opts.agentName ?? HerdrBackend.defaultAgentName();
if (opts.externalTarget?.paneId) this.paneId = opts.externalTarget.paneId;
}

Expand Down Expand Up @@ -537,6 +544,23 @@ export class HerdrBackend implements SessionBackend {
if (existing) {
this.actuallyReattached = true;
this.paneId = existing.pane_id;
} else if (this.opts.isReattach) {
// FREEZE the reattach decision (mirrors ZmxBackend: "never turn a stale
// reattach into a new CLI after the backing session disappeared"). The
// worker predicted reattach from an earlier probe and therefore SKIPPED
// the cold-path setup that only runs on !willReattachPersistent — the
// PENDING generation proof AND the credential-only Seatbelt/bwrap wrapper.
// If the `botmux` agent vanished between that probe and here, silently
// `agent start`ing a fresh CLI would launch it WITHOUT the credential
// boundary (unsafe on an enrolled host) and leave the old committed marker
// in place to later reattach it as "isolated". Post-spawn teardown can't
// undo an already-executed unwrapped CLI, so we must refuse HERE: throw so
// the worker's next launch takes the cold path (write PENDING + assemble
// the wrapper BEFORE creating the agent).
throw new Error(
`herdr agent ${this.agentName} in ${this.sessionName} disappeared before reattach; `
+ `refusing to silently start a fresh (unwrapped) generation`,
);
} else if (herdrUsesPaneAgentStart()) {
this.paneId = this.startPaneAgent(bin, args, opts);
} else {
Expand Down
63 changes: 53 additions & 10 deletions src/adapters/backend/session-backend-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,16 +362,59 @@ export function selectSessionBackend(opts: {
+ 'close it explicitly before enabling isolation or MCP',
);
}
} else if (HerdrBackend.hasSession(ownedSessionName)) {
return {
backend: new HerdrBackend(ownedSessionName, { isReattach: true }),
isTmuxMode: false,
isPipeMode: true,
isZellijMode: false,
persistentSessionName: ownedSessionName,
persistentBackendTarget: { backendType: 'herdr', sessionName: ownedSessionName },
isReattach: true,
};
} else {
// Owned isolation/MCP host. The reattach decision must be AGENT-precise, not
// session-level: herdr can keep a live host session whose `botmux` agent row
// has disappeared (killSession's own comment records that session dir, agent
// metadata and process state diverge). Predicting reattach from the SESSION
// alone would, when the agent is gone, make HerdrBackend.spawn's frozen
// reattach guard throw every launch (kill-loop) — and the worker would have
// skipped the cold-path setup (PENDING proof + credential-only wrapper, gated
// on !willReattachPersistent), so a silent fresh-start would run UNWRAPPED.
//
// Use TRI-STATE probes (never `hasSession && hasAgent` — those collapse
// `unknown` to false and re-introduce fail-open). Table:
// host unknown → refuse (no kill, no spawn)
// host missing → fall through to the shared-host cold path
// host exists, agent unknown → refuse
// host exists, agent exists → reattach the same owned host
// host exists, agent missing → COLD start IN the same owned host
// (isReattach:false → worker writes
// PENDING + assembles the wrapper, then
// Herdr `agent start`s a new generation);
// no teardown of the still-live host.
const ownedAgentName = HerdrBackend.defaultAgentName();
const hostProbe = HerdrBackend.probeSession(ownedSessionName);
if (hostProbe === 'unknown') {
throw new Error(
`owned herdr session ${ownedSessionName} probe inconclusive; `
+ 'refusing isolation/MCP reattach-vs-fresh decision',
);
}
if (hostProbe === 'exists') {
const agentProbe = HerdrBackend.probeAgent(ownedSessionName, ownedAgentName);
if (agentProbe === 'unknown') {
throw new Error(
`owned herdr agent ${ownedAgentName} in ${ownedSessionName} probe inconclusive; `
+ 'refusing isolation/MCP reattach-vs-fresh decision',
);
}
const agentLive = agentProbe === 'exists';
return {
backend: new HerdrBackend(ownedSessionName, {
// agent missing on a live host → in-place cold start (create the agent,
// NOT the session, which already exists).
isReattach: agentLive,
}),
isTmuxMode: false,
isPipeMode: true,
isZellijMode: false,
persistentSessionName: ownedSessionName,
persistentBackendTarget: { backendType: 'herdr', sessionName: ownedSessionName },
isReattach: agentLive,
};
}
// host missing → fall through to the shared-host cold path below.
}

// Every fresh agent actively launched by this machine's Botmux shares the
Expand Down
Loading
Loading